802 lines
31 KiB
PHP
802 lines
31 KiB
PHP
<?php
|
|
|
|
namespace objects;
|
|
|
|
use classes\db;
|
|
use classes\object_property;
|
|
use classes\slack;
|
|
use classes\stripe;
|
|
use Exception;
|
|
use traits\db_object_t;
|
|
|
|
class departments_o extends db
|
|
{
|
|
use db_object_t;
|
|
|
|
public object_property $name;
|
|
public object_property $description;
|
|
public object_property $economic_department_id; // The id of the department in the economic system (Can be null)
|
|
public object_property $slack_webhook; // The slack webhook for the department
|
|
public department_variables_o $variables; // The department variables object
|
|
public object_property $dimension; // The dimension of the department
|
|
public object_property $visible; // The visibility of the department
|
|
public object_property $branding; // The branding of the department
|
|
public object_property $longitude; // The longitude of the department (Can be null)
|
|
public object_property $latitude; // The latitude of the department (Can be null)
|
|
public object_property $order_priority; // The order priority of the department (Lower number = higher priority)
|
|
public object_property $created_at;
|
|
public object_property $updated_at;
|
|
|
|
public function structure(): void
|
|
{
|
|
$this->setTable('departments');
|
|
}
|
|
|
|
public function objectChanged(): void
|
|
{
|
|
// Clear the cache
|
|
redis->clear_departments();
|
|
}
|
|
|
|
/**
|
|
* Get the lanes associated with the department
|
|
* @retuns department_lanes_o[] The lanes associated with the department
|
|
* @throws Exception If the department is not selected
|
|
*/
|
|
public function getLanes(): array
|
|
{
|
|
self::requireSelected();
|
|
return (new department_lanes_o())->getDepartmentLanes($this->id);
|
|
}
|
|
|
|
/**
|
|
* Add a department
|
|
* @param string $name
|
|
* @param string $description
|
|
* @throws Exception If the object was not created successfully
|
|
*/
|
|
public function create(string $name, string $description): void
|
|
{
|
|
global $db;
|
|
// Avoid SQL injection
|
|
$name = $db->escape_string($name);
|
|
$description = $db->escape_string($description);
|
|
// Create a new record in the database
|
|
$sql = "INSERT INTO $this->table (name, description) VALUES ('$name', '$description')";
|
|
$db->query($sql);
|
|
|
|
// Get the id of the new record
|
|
$this->id = $db->insert_id();
|
|
|
|
// Set the values of the object properties
|
|
$this->getObjectProperties();
|
|
|
|
// Clear the cache
|
|
redis->clear_departments();
|
|
}
|
|
|
|
/**
|
|
* Get the object properties of the department
|
|
* @throws Exception If the object is not selected
|
|
*/
|
|
public function getObjectProperties(): void
|
|
{
|
|
self::requireSelected();
|
|
$this->name = new object_property($this->table, $this->id, 'name', 'string', true);
|
|
$this->description = new object_property($this->table, $this->id, 'description', 'string', false);
|
|
$this->economic_department_id = new object_property($this->table, $this->id, 'economic_department_id', 'int', false);
|
|
$this->slack_webhook = new object_property($this->table, $this->id, 'slack_webhook', 'string', false);
|
|
$this->variables = (new department_variables_o())->selectDepartment($this->id);
|
|
$this->dimension = new object_property($this->table, $this->id, 'dimension', 'int', false);
|
|
$this->branding = new object_property($this->table, $this->id, 'branding', 'int', false);
|
|
$this->visible = new object_property($this->table, $this->id, 'visible', 'int', false);
|
|
$this->longitude = new object_property($this->table, $this->id, 'longitude', 'float', false);
|
|
$this->latitude = new object_property($this->table, $this->id, 'latitude', 'float', false);
|
|
$this->order_priority = new object_property($this->table, $this->id, 'order_priority', 'int', false);
|
|
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'string', false);
|
|
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'string', false);
|
|
}
|
|
|
|
public function edit(int $id, string $name, string $description, int $economic_department_id): void
|
|
{
|
|
global $db;
|
|
$this->id = $id;
|
|
// Avoid SQL injection
|
|
$name = $db->escape_string($name);
|
|
$description = $db->escape_string($description);
|
|
// If the economic_department_id is 0, set it to null
|
|
if ($economic_department_id === 0) {
|
|
$economic_department_id = null;
|
|
}
|
|
// Update the record in the database
|
|
$sql = "UPDATE $this->table SET name = '$name', description = '$description', economic_department_id = " . ($economic_department_id ?? 'NULL') . " WHERE id = $this->id";
|
|
$db->query($sql);
|
|
|
|
// Set the values of the object properties
|
|
$this->getObjectProperties();
|
|
|
|
// Clear the cache
|
|
redis->clear_department($id);
|
|
}
|
|
|
|
public function list($superUser = false): array
|
|
{
|
|
global $db;
|
|
// Check if the departments are cached
|
|
$departments = redis->get_departments();
|
|
// If the departments are not cached, get them from the database
|
|
if ($departments === null) {
|
|
$sql = "SELECT * FROM $this->table";
|
|
$result = $db->query($sql);
|
|
$departments = $db->fetch_all($result);
|
|
// Cache the departments (If it is not empty or null)
|
|
if (!empty($departments)) {
|
|
redis->cache_departments($departments);
|
|
}
|
|
}
|
|
// Only show the name, description and id if the user isn't a super user
|
|
if (!$superUser) {
|
|
// Map the departments to only show the name, description and id
|
|
$departments = array_map(function ($department) {
|
|
return [
|
|
'name' => $department['name'],
|
|
'description' => $department['description'],
|
|
'id' => $department['id'],
|
|
'visible' => $department['visible'],
|
|
];
|
|
}, $departments);
|
|
}
|
|
return $departments;
|
|
}
|
|
|
|
public function getDepartmentById(int $id, bool $force_all = false): array
|
|
{
|
|
global $db;
|
|
// Check if the department is cached
|
|
$department = redis->get_department($id);
|
|
// If the department is not cached, get it from the database
|
|
if ($department === null || $force_all) {
|
|
$sql = "SELECT * FROM $this->table WHERE id = $id";
|
|
$result = $db->query($sql);
|
|
$department = $db->fetch_assoc($result);
|
|
// Cache the department (If it is not empty or null)
|
|
if (!empty($department)) {
|
|
redis->cache_department($id, $department);
|
|
}
|
|
}
|
|
return $department;
|
|
}
|
|
|
|
/**
|
|
* Get the price of a product in a department
|
|
* @param int $department_id
|
|
* @param int $product_id
|
|
* @param int $price
|
|
* @return void
|
|
*/
|
|
public function setDepartmentProductPrice(int $department_id, int $product_id, int $price): void
|
|
{
|
|
global $db;
|
|
// Check if the record already exists
|
|
$this->removeDepartmentProductPriceIfExist($department_id, $product_id);
|
|
// If the price is 0, return
|
|
if ($price === 0) {
|
|
return;
|
|
}
|
|
// Create a new record in the database
|
|
$sql = "INSERT INTO product_department_prices (department_id, product_id, price) VALUES ($department_id, $product_id, $price)";
|
|
$db->query($sql);
|
|
}
|
|
|
|
/**
|
|
* Remove the price of a product in a department
|
|
* @param int $department_id
|
|
* @param int $product_id
|
|
* @return void
|
|
*/
|
|
private function removeDepartmentProductPriceIfExist(int $department_id, int $product_id): void
|
|
{
|
|
global $db;
|
|
// Get the price from the database
|
|
$sql = "SELECT * FROM product_department_prices WHERE department_id = $department_id AND product_id = $product_id";
|
|
$result = $db->query($sql);
|
|
|
|
if ($result->num_rows > 0) {
|
|
// Remove the record from the database
|
|
$sql = "DELETE FROM product_department_prices WHERE department_id = $department_id AND product_id = $product_id";
|
|
$db->query($sql);
|
|
}
|
|
}
|
|
|
|
public function getDepartmentProductPrices(int $department_id): array
|
|
{
|
|
global $db;
|
|
$sql = "SELECT * FROM product_department_prices WHERE department_id = $department_id";
|
|
$result = $db->query($sql);
|
|
return $db->fetch_all($result);
|
|
}
|
|
|
|
public function getDepartmentName(int $department): string
|
|
{
|
|
global $db;
|
|
// Check if the department name is cached
|
|
$name = redis->get_department_name($department);
|
|
// If the department name is not cached, get it from the database
|
|
if ($name === null) {
|
|
try {
|
|
$this->id = $department;
|
|
$this->getObjectProperties();
|
|
$name = $this->name->value();
|
|
// Cache the department name (If it is not empty or null)
|
|
if (!empty($name)) {
|
|
redis->cache_department_name($department, $name);
|
|
}
|
|
} catch (Exception $e) {
|
|
$name = 'Unable to get department name: ' . $department;
|
|
}
|
|
return $name;
|
|
}
|
|
return redis->get_department_name($department) ?? 'Unable to get department name: ' . $department;
|
|
}
|
|
|
|
/**
|
|
* Get the Stripe terminal location department variable
|
|
* @note This is a link to the department_variables_o::getVariable(stripe_terminal_location) method
|
|
* @return string The Stripe terminal location id
|
|
* @throws Exception If the department variable is not set
|
|
* @throws Exception If the department is not selected
|
|
*/
|
|
public function getStripeTerminalLocation(): string
|
|
{
|
|
self::requireSelected();
|
|
if (!$this->variables->isSet('stripe_terminal_location')) {
|
|
throw new Exception('The Stripe terminal location is not set for the department.');
|
|
}
|
|
return $this->variables->getVariable('stripe_terminal_location');
|
|
}
|
|
|
|
/**
|
|
* Check if the Stripe terminal location variable is set
|
|
* @note This is a link to the department_variables_o::isSet(stripe_terminal_location) method
|
|
* @return bool true If the Stripe terminal location is set
|
|
* @throws Exception If the department is not selected
|
|
*/
|
|
public function isStripeTerminalLocationSet(): bool
|
|
{
|
|
self::requireSelected();
|
|
return $this->variables->isSet('stripe_terminal_location');
|
|
}
|
|
|
|
/**
|
|
* Set the Stripe terminal location department variable
|
|
* @note This is a link to the department_variables_o::set(stripe_terminal_location) method
|
|
* @param string $location The Stripe terminal location id
|
|
* @throws Exception If the department is not selected
|
|
* @throws Exception If the department variable is not updated successfully
|
|
*/
|
|
public function setStripeTerminalLocation(string $location): void
|
|
{
|
|
self::requireSelected();
|
|
$this->variables->set('stripe_terminal_location', $location);
|
|
}
|
|
|
|
/**
|
|
* Get the Stripe terminal readers
|
|
* @note This is a link to the stripe_endpoint_readers::listLocation method
|
|
* @throws Exception If the department is not selected
|
|
* @throws Exception If the Stripe terminal location is not set for the department
|
|
*/
|
|
public function getStripeTerminalReaders(): \Stripe\Collection
|
|
{
|
|
self::requireSelected();
|
|
if (!$this->variables->isSet('stripe_terminal_location')) {
|
|
throw new Exception('The Stripe terminal location is not set for the department.');
|
|
}
|
|
return (new stripe())->readers->listLocation($this->variables->getVariable('stripe_terminal_location'));
|
|
}
|
|
|
|
/**
|
|
* Convert the object to an array
|
|
* @param array|null $options The options for the conversion (e.g. ['slack_webhook' => true])
|
|
* @return array The object as an array
|
|
* @throws Exception If the object is not selected
|
|
*/
|
|
public function asArray(array $options = null): array
|
|
{
|
|
self::requireSelected();
|
|
$tmp = [
|
|
'id' => (int)$this->id,
|
|
'name' => (string)$this->name->value(),
|
|
'description' => (string)$this->description->value(),
|
|
'economic_department_id' => (int)$this->economic_department_id->value(),
|
|
'dimension' => (int)$this->dimension->value(),
|
|
'branding' => (int)$this->branding->value(),
|
|
'longitude' => (float)$this->longitude->value(),
|
|
'latitude' => (float)$this->latitude->value(),
|
|
'order_priority' => (int)$this->order_priority->value(),
|
|
'created_at' => (string)$this->created_at->value(),
|
|
'updated_at' => (string)$this->updated_at->value(),
|
|
];
|
|
// Check if the webhook should be included
|
|
if ($options && $options['slack_webhook']) {
|
|
$tmp['slack_webhook'] = (string)$this->slack_webhook->value();
|
|
}
|
|
return $tmp;
|
|
}
|
|
|
|
/**
|
|
* Check if the Stripe is configured for the department
|
|
* @note This is a link to the department_variables_o::isSet(stripe_terminal_location) method
|
|
* @return bool true If the Stripe is configured
|
|
* @throws Exception If the department is not selected
|
|
*/
|
|
public function isStripeConfigured(): bool
|
|
{
|
|
self::requireSelected();
|
|
if ($this->variables->isSet('stripe_terminal_location')) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @throws Exception If the department is not selected
|
|
* @throws Exception If the branding is not set
|
|
* @throws Exception If the branding is not found
|
|
*/
|
|
public function getBranding(): branding_o
|
|
{
|
|
self::requireSelected();
|
|
return (new branding_o())->select($this->branding->value());
|
|
}
|
|
|
|
/**
|
|
* Get the phone numbers for the SMS notification
|
|
* @note This is a link to the department_notification_sms_o::getDepartmentActivePhoneNumbers method
|
|
* @throws Exception If the department is not selected
|
|
*/
|
|
public function notificationSmsPhoneNumbers(): array
|
|
{
|
|
self::requireSelected();
|
|
return (new department_notification_sms_o())->getDepartmentActivePhoneNumbers($this->id);
|
|
}
|
|
|
|
/**
|
|
* @throws Exception
|
|
*/
|
|
public function isModuleTimeBookingsEnabled(): bool
|
|
{
|
|
self::requireSelected();
|
|
return (bool)$this->variables->getVariable('bookingsystem_time_based_enabled');
|
|
}
|
|
|
|
/**
|
|
* Select a department by its name (case-sensitive, exact match)
|
|
* @throws Exception If the department selection fails or the department is not found
|
|
* @example "Hvidovre"
|
|
*/
|
|
public function selectByName(string $departmentName): self
|
|
{
|
|
$result = self::getFieldsWhere([
|
|
'name' => $departmentName,
|
|
], [
|
|
'id',
|
|
]);
|
|
if (empty($result)) {
|
|
throw new Exception('Department not found: ' . $departmentName);
|
|
}
|
|
$this->selectId($result[0]['id']);
|
|
return $this;
|
|
}
|
|
|
|
public function selectId(int $department_id): self
|
|
{
|
|
$this->id = $department_id;
|
|
$this->getObjectProperties();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return products_o[] An array of all products in the department's categories
|
|
* @throws Exception
|
|
*/
|
|
public function getAllProductInDepartmentCategories(): array
|
|
{
|
|
self::requireSelected();
|
|
|
|
// Get all the categories associated
|
|
$department_categories = (new department_categories_o())->getDepartmentCategoryObjects($this->id);
|
|
|
|
// Use array_reduce to combine both loops into a single operation
|
|
$products = array_reduce($department_categories, function ($products, $department_category) {
|
|
return array_merge($products, $department_category->getCategory()->getProducts());
|
|
}, []);
|
|
|
|
// Remove duplicate products
|
|
return array_unique($products, SORT_REGULAR);
|
|
}
|
|
|
|
/**
|
|
* If the department is excluded from invoicing
|
|
* @return bool
|
|
* @throws Exception If the department is not selected
|
|
*/
|
|
public function isExcludedFromInvoicing(): bool
|
|
{
|
|
self::requireSelected();
|
|
return $this->variables->getVariable('exclude_from_invoicing') === true;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Send department period statistics to Slack
|
|
* @param string $start_date (YYYY-MM-DD)
|
|
* @param string $end_date (YYYY-MM-DD)
|
|
* @return void
|
|
* @throws Exception If the department is not selected
|
|
*/
|
|
public function sendPeriodStatisticsToSlack(string $start_date, string $end_date): void
|
|
{
|
|
self::requireSelected();
|
|
// Configuration
|
|
$department_id = $this->id;
|
|
$product_ids = [
|
|
25,
|
|
[23, 24], // Used to merge two products into one percentage (Spot Free)
|
|
22,
|
|
27,
|
|
21,
|
|
26
|
|
];
|
|
$date_end = date('Y-m-d 23:59:59', strtotime($end_date)); // End date at 23:59:59
|
|
$date_start = date('Y-m-d 00:00:00', strtotime($start_date)); // Start date at 00:00:00
|
|
/**
|
|
* Weekly results for Roskilde
|
|
*
|
|
* Period: 29.01.2026 - 04.02.2026
|
|
* Washes: 67
|
|
* Fælg Flex: 55%
|
|
* Spot Free: 22%
|
|
* Special Sæbe: 11%
|
|
* 10 min ekstra: 15%
|
|
* Undervognsskyl: 44%
|
|
* Voks: 66%
|
|
*/
|
|
$max_addons = [];
|
|
$sold_addons = [];
|
|
$percentages = [];
|
|
$department = (new departments_o())->select((int)$this->id);
|
|
$tmp = "*Weekly results for {$department->name->value()}*\n";
|
|
$tmp .= "Period: " . date('d.m.Y', strtotime($date_start)) . " - " . date('d.m.Y', strtotime($date_end)) . "\n";
|
|
// Washes
|
|
$wash_count = (new orders_o())->countWashesInDateRange($date_start, $date_end, $department_id);
|
|
$analytics = $this->analyzeAddonSalesData($product_ids, $date_start, $date_end, $department_id, $max_addons, $sold_addons, $percentages);
|
|
$tmp .= "Washes: $wash_count\n" . implode('', $analytics);
|
|
|
|
// Check if there's a custom webhook for the department
|
|
$slack = new slack();
|
|
if (empty($this->slack_webhook->value())) {
|
|
// If no webhook is set for the department, use the default Slack notification
|
|
$slack->send_message($tmp);
|
|
} else {
|
|
// Set the custom webhook for the department
|
|
$slack->send_webhook_message($tmp, $this->slack_webhook->value());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array $product_ids
|
|
* @param string $date_start
|
|
* @param string $date_end
|
|
* @param int|array $department_id
|
|
* @param array $max_addons
|
|
* @param array $sold_addons
|
|
* @param array $percentages
|
|
* @return array
|
|
* @throws Exception
|
|
*/
|
|
public function analyzeAddonSalesData(array $product_ids, string $date_start, string $date_end, int|array $department_id, array $max_addons, array $sold_addons, array $percentages): array
|
|
{
|
|
if (is_array($department_id)) {
|
|
// Combine percentages from multiple departments
|
|
$combined_percentages = [];
|
|
foreach ( $department_id as $dept_id ) {
|
|
$dept_percentages = $this->getThePercentageOfAddonsSoldOutOfMax($product_ids, $date_start, $date_end, $dept_id, $max_addons, $sold_addons, $percentages);
|
|
foreach ( $dept_percentages as $key => $value ) {
|
|
if (!isset($combined_percentages[$key])) {
|
|
$combined_percentages[$key] = 0;
|
|
}
|
|
$combined_percentages[$key] += $value;
|
|
}
|
|
}
|
|
$percentages = $combined_percentages;
|
|
} else {
|
|
// Single department
|
|
$percentages = $this->getThePercentageOfAddonsSoldOutOfMax($product_ids, $date_start, $date_end, $department_id, $max_addons, $sold_addons, $percentages);
|
|
}
|
|
$tmp = [];
|
|
foreach ( $product_ids as $product_id ) {
|
|
if (is_array($product_id)) {
|
|
// Merged product names
|
|
$product_names = [];
|
|
foreach ( $product_id as $pid ) {
|
|
$product_names[] = (new products_o())->select($pid)->name->value();
|
|
}
|
|
// Switch to joined names
|
|
$product_name = match (true) {
|
|
in_array(23, $product_id) && in_array(24, $product_id) => 'Spot Free',
|
|
default => implode(' + ', $product_names),
|
|
};
|
|
} else {
|
|
// Check single product name
|
|
$product_name = match ($product_id) {
|
|
25 => 'Fælg Flex',
|
|
22 => 'Special Sæbe',
|
|
27 => '10 min ekstra',
|
|
21 => 'Undervognsskyl',
|
|
26 => 'Voks',
|
|
default => (new products_o())->select($product_id)->name->value(),
|
|
};
|
|
}
|
|
$tmp[] = "{$product_name}: {$percentages[is_array($product_id) ? implode('_', $product_id) : $product_id]}%\n";
|
|
}
|
|
// Return the data
|
|
return $tmp;
|
|
}
|
|
|
|
/**
|
|
* @param array $product_ids
|
|
* @param string $date_start
|
|
* @param string $date_end
|
|
* @param int $department_id
|
|
* @param array $max_addons
|
|
* @param array $sold_addons
|
|
* @param array $percentages
|
|
* @return array
|
|
*/
|
|
public function getThePercentageOfAddonsSoldOutOfMax(array $product_ids, string $date_start, string $date_end, int $department_id, array &$max_addons, array &$sold_addons, array &$percentages): array
|
|
{
|
|
$product_options_o = new product_options_o();
|
|
|
|
// Get the percentage of addons sold out of max
|
|
foreach ( $product_ids as $product_id ) {
|
|
// Handle merged products
|
|
if (is_array($product_id)) {
|
|
$addon_sold_count = 0;
|
|
$addon_max_count = 0;
|
|
foreach ( $product_id as $pid ) {
|
|
$pid = (int)$pid;
|
|
$addon_sold_count += $product_options_o->countSoldAddonsInDateRange($pid, $date_start, $date_end, $department_id);
|
|
$addon_max_count += $product_options_o->getMaxAddonsInDateRange($pid, $date_start, $date_end, $department_id);
|
|
}
|
|
} else {
|
|
$pid = $product_id;
|
|
$addon_sold_count = $product_options_o->countSoldAddonsInDateRange($pid, $date_start, $date_end, $department_id);
|
|
$addon_max_count = $product_options_o->getMaxAddonsInDateRange($pid, $date_start, $date_end, $department_id);
|
|
}
|
|
// Prevent division by zero
|
|
if ($addon_max_count === 0) {
|
|
$addon_percentage_sold = 0;
|
|
} else {
|
|
$addon_percentage_sold = ($addon_sold_count / $addon_max_count) * 100;
|
|
}
|
|
$max_addons[is_array($product_id) ? implode('_', $product_id) : $product_id] = $addon_max_count;
|
|
$sold_addons[is_array($product_id) ? implode('_', $product_id) : $product_id] = $addon_sold_count;
|
|
$percentages[is_array($product_id) ? implode('_', $product_id) : $product_id] = number_format($addon_percentage_sold, 2);
|
|
}
|
|
return $percentages;
|
|
}
|
|
|
|
/**
|
|
* Get the total amount of addons sold in a department in a date range
|
|
* @param array $product_ids
|
|
* @param string $date_start
|
|
* @param string $date_end
|
|
* @param int $department_id
|
|
* @return float
|
|
*/
|
|
public function getTotalPercentageAddonsSoldInDepartment(array $product_ids, string $date_start, string $date_end, int $department_id): float
|
|
{
|
|
$max_addons = [];
|
|
$sold_addons = [];
|
|
$percentages = [];
|
|
|
|
$this->getThePercentageOfAddonsSoldOutOfMax($product_ids, $date_start, $date_end, $department_id, $max_addons, $sold_addons, $percentages);
|
|
|
|
$total_sold = array_sum($sold_addons);
|
|
$total_max = array_sum($max_addons);
|
|
|
|
if ($total_max === 0) {
|
|
return 0;
|
|
}
|
|
|
|
return ($total_sold / $total_max) * 100;
|
|
}
|
|
|
|
/**
|
|
* Get the total maximum addons available in a department in a date range
|
|
* @param array $product_ids
|
|
* @param string $date_start
|
|
* @param string $date_end
|
|
* @param int $department_id
|
|
* @return float
|
|
*/
|
|
public function getTotalPercentageMaxAddonsInDepartment(array $product_ids, string $date_start, string $date_end, int $department_id): float
|
|
{
|
|
return $this->getTotalPercentageAddonsSoldInDepartment($product_ids, $date_start, $date_end, $department_id);
|
|
}
|
|
|
|
/**
|
|
* Get the total amount of addons sold in a department in a date range
|
|
* @param array $product_ids
|
|
* @param string $date_start
|
|
* @param string $date_end
|
|
* @param int $department_id
|
|
* @return int
|
|
* @throws Exception
|
|
*/
|
|
public function getTotalAddonsSoldInDepartment(array $product_ids, string $date_start, string $date_end, int $department_id): int
|
|
{
|
|
$total_addons_sold = 0;
|
|
foreach ($product_ids as $product_id) {
|
|
$addon_sold_count = (new product_options_o())->countSoldAddonsInDateRange($product_id, $date_start, $date_end, $department_id);
|
|
$total_addons_sold += $addon_sold_count;
|
|
}
|
|
return $total_addons_sold;
|
|
}
|
|
/**
|
|
* Get the total maximum addons available in a department in a date range
|
|
* @param array $product_ids
|
|
* @param string $date_start
|
|
* @param string $date_end
|
|
* @param int $department_id
|
|
* @return int
|
|
* @throws Exception
|
|
*/
|
|
public function getTotalMaxAddonsInDepartment(array $product_ids, string $date_start, string $date_end, int $department_id): int
|
|
{
|
|
return self::methodCacheWithParameters(__METHOD__, func_get_args(), 120, function() use ($product_ids, $date_start, $date_end, $department_id) {
|
|
$total_max_addons = 0;
|
|
foreach ($product_ids as $product_id) {
|
|
$addon_max_count = (new product_options_o())->getMaxAddonsInDateRange($product_id, $date_start, $date_end, $department_id);
|
|
$total_max_addons += $addon_max_count;
|
|
}
|
|
return $total_max_addons;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send a (truck wash internal) department statistic Slack notification
|
|
* @param string $date_start (YYYY-MM-DD)
|
|
* @param string $date_end (YYYY-MM-DD)
|
|
* @param int[] $department_ids
|
|
* @param int[] $product_ids
|
|
* @return void
|
|
* @throws Exception
|
|
*/
|
|
public function sendSlackInternalStatisticNotification(string $date_start, string $date_end, array $department_ids, array $product_ids = [
|
|
25,
|
|
[23, 24], // Used to merge two products into one percentage (Spot Free)
|
|
22,
|
|
27,
|
|
21,
|
|
26
|
|
]): void
|
|
{
|
|
if (empty($department_ids)) {
|
|
throw new Exception('No department ids provided for the Slack internal statistic notification.');
|
|
}
|
|
if (empty($product_ids)) {
|
|
throw new Exception('No product ids provided for the Slack internal statistic notification.');
|
|
}
|
|
|
|
// Sort the departments by order_priority (lowest => first)
|
|
$departments_data = $this->getFieldsWhereIn(['id' => $department_ids], ['id', 'order_priority']);
|
|
usort($departments_data, function ($a, $b) {
|
|
return (int)$a['order_priority'] <=> (int)$b['order_priority'];
|
|
});
|
|
$department_ids = array_column($departments_data, 'id');
|
|
|
|
// Configuration
|
|
$date_end = date('Y-m-d 23:59:59', strtotime($date_end)); // End date at 23:59:59
|
|
$date_start = date('Y-m-d 00:00:00', strtotime($date_start)); // Start date at 00:00:00
|
|
|
|
/**
|
|
* Weekly results for Truck Wash
|
|
*
|
|
* Period: 29.01.2026 - 04.02.2026
|
|
* Washes:
|
|
*
|
|
* Taastrup: 300
|
|
* Hvidovre: 52
|
|
* Glostrup: 121
|
|
* Køge: 54
|
|
* Roskilde: 65
|
|
* Aarhus C: 32
|
|
* Taulov: 122
|
|
* - Total: 650
|
|
* Undervognsskyl
|
|
*
|
|
* Taastrup: 55%
|
|
* Hvidovre: 22%
|
|
* Glostrup: 22%
|
|
* Køge: 24%
|
|
* Roskilde: 54%
|
|
* Aarhus C: 66%
|
|
* Taulov: 55%
|
|
* - Total: 55%
|
|
* ...
|
|
*/
|
|
$tmp = "*Weekly results for Truck Wash*\n";
|
|
$tmp .= "_Period: " . date('d.m.Y', strtotime($date_start)) . " - " . date('d.m.Y', strtotime($date_end)) . "_\n";
|
|
// Washes
|
|
$total_wash_count = 0;
|
|
$tmp .= "*Washes:*\n";
|
|
$orders_o = new orders_o();
|
|
foreach ( $department_ids as $department_id ) {
|
|
$wash_count = $orders_o->countWashesInDateRange($date_start, $date_end, $department_id);
|
|
$total_wash_count += $wash_count;
|
|
$department = (new departments_o())->select($department_id);
|
|
$tmp .= "> {$department->name->value()}: $wash_count\n";
|
|
}
|
|
$tmp .= "> - Total: {$total_wash_count}\n";
|
|
|
|
// Pre-instantiate products_o to reuse
|
|
$products_o = new products_o();
|
|
$max_addons = [];
|
|
$sold_addons = [];
|
|
$percentages = [];
|
|
|
|
// Loop through each product and get the percentages for each department
|
|
foreach ( $product_ids as $product_id ) {
|
|
// Reset totals for each product
|
|
$total_percentage = 0;
|
|
$total_count = 0;
|
|
$total_sold = 0;
|
|
|
|
// Header for the product
|
|
if (is_array($product_id)) {
|
|
$product_names = [];
|
|
foreach ( $product_id as $pid ) {
|
|
$products_o->select($pid);
|
|
$product_names[] = $products_o->name->value();
|
|
}
|
|
$product_name = match (true) {
|
|
in_array(23, $product_id) && in_array(24, $product_id) => 'Spot Free',
|
|
default => implode(' + ', $product_names),
|
|
};
|
|
} else {
|
|
$product_name = match ($product_id) {
|
|
25 => 'Fælg Flex',
|
|
22 => 'Special Sæbe',
|
|
27 => '10 min ekstra',
|
|
21 => 'Undervognsskyl',
|
|
26 => 'Voks',
|
|
default => $products_o->select($product_id)->name->value(),
|
|
};
|
|
}
|
|
$tmp .= "\n*{$product_name}:*\n";
|
|
$product_key = is_array($product_id) ? implode('_', $product_id) : $product_id;
|
|
|
|
// Loop through each department to get the percentage
|
|
foreach ( $department_ids as $department_id ) {
|
|
$department = (new departments_o())->select($department_id);
|
|
$this->getThePercentageOfAddonsSoldOutOfMax([$product_id], $date_start, $date_end, $department_id, $max_addons, $sold_addons, $percentages);
|
|
$percentage = $percentages[$product_key];
|
|
$total_count += $max_addons[$product_key];
|
|
$total_sold += $sold_addons[$product_key];
|
|
$tmp .= "> {$department->name->value()}: {$percentage}%\n";
|
|
}
|
|
// Get the total percentage for the product
|
|
if ($total_count !== 0) {
|
|
// Prevent division by zero
|
|
$total_percentage = ($total_sold / $total_count) * 100;
|
|
}
|
|
$tmp .= "> - Total: " . number_format($total_percentage, 2) . "%\n";
|
|
}
|
|
// Send the message to the internal Slack webhook
|
|
$slack = new slack();
|
|
$slack->send_webhook_message($tmp, (new departments_o())->select(10)->slack_webhook->value());
|
|
}
|
|
} |