Files
api/services/nginx/app/modules/goals/classes/goals_criteria.php
T
Jeppe Bundgaard 0491a2ad1b Add VISITS criteria type and implement visit counting logic in goals_criteria
- Introduce `VISITS` as a new goal criteria type in `goals_criteria_type` enum.
- Extend `goals_criteria` with `countVisits` method to compute unique visit counts based on criteria filters.
- Update result computation logic in `goals_criteria` to support `VISITS`.
2026-01-26 19:24:51 +01:00

261 lines
8.9 KiB
PHP

<?php
namespace goals\classes;
use goals\helpers\goals_criteria_type;
use goals\interfaces\goals_criteria_i;
use goals\traits\goals_result_parser_t;
use goals\traits\goals_target_t;
use goals\traits\goals_timeframe_t;
use goals\traits\goals_label_t;
use objects\order_items_o;
class goals_criteria implements goals_criteria_i
{
use goals_timeframe_t,
goals_target_t,
goals_result_parser_t,
goals_label_t;
/**
* The users criteria
* @var goals_criteria_users $users
*/
public goals_criteria_users $users;
/**
* The departments criteria
* @var goals_criteria_departments $departments
*/
public goals_criteria_departments $departments;
/**
* Criteria type (products sold, etc.)
* @var goals_criteria_type $type
*/
public goals_criteria_type $type = goals_criteria_type::NONE;
/**
* Products criteria
* @var goals_criteria_products|null $products
*/
public ?goals_criteria_products $products;
/**
* Constructor
*/
public function __construct()
{
$this->users = new goals_criteria_users();
$this->departments = new goals_criteria_departments();
$this->products = new goals_criteria_products();
$this->start = new \DateTime();
$this->end = new \DateTime();
}
/**
* @throws \Exception If the JSON is invalid
*/
public static function fromJson(string $json): goals_criteria
{
$data = json_decode($json, true);
$criteria = new goals_criteria();
if (isset($data['type'])) {
$criteria->type = goals_criteria_type::tryFrom($data['type']);
}
if (isset($data['target'])) {
$criteria->target = $data['target'];
}
if (isset($data['label'])) {
$criteria->label = is_string($data['label']) ? $data['label'] : null;
}
if (isset($data['start'])) {
$criteria->start = new \DateTime($data['start']);
}
if (isset($data['end'])) {
$criteria->end = new \DateTime($data['end']);
}
// Parse users (expects array of customer_numbers or objects with customer_number)
if (isset($data['users']) && is_array($data['users'])) {
$users = array_map(function ($item) {
$customerNumber = null;
if (is_array($item)) {
if (isset($item['customer_number'])) {
$customerNumber = (int)$item['customer_number'];
} elseif (isset($item['customerNumber'])) { // allow camelCase
$customerNumber = (int)$item['customerNumber'];
}
} elseif (is_numeric($item)) {
$customerNumber = (int)$item;
}
if ($customerNumber === null) {
return null;
}
return (new \objects\users_o())->getUserByCustomerNumber($customerNumber);
}, $data['users']);
// Filter out nulls in case of malformed entries
$users = array_values(array_filter($users));
$criteria->users->set($users);
}
// Parse departments (expects array of IDs or objects with id)
if (isset($data['departments']) && is_array($data['departments'])) {
$departments = array_map(function ($item) {
$id = null;
if (is_array($item)) {
if (isset($item['id'])) {
$id = (int)$item['id'];
}
} elseif (is_numeric($item)) {
$id = (int)$item;
}
if ($id === null) {
return null;
}
return (new \objects\departments_o())->select($id);
}, $data['departments']);
$departments = array_values(array_filter($departments));
$criteria->departments->set($departments);
}
// Parse products (expects array of IDs or objects with id)
if (isset($data['products']) && is_array($data['products'])) {
$products = array_map(function ($item) {
$id = null;
if (is_array($item)) {
if (isset($item['id'])) {
$id = (int)$item['id'];
}
} elseif (is_numeric($item)) {
$id = (int)$item;
}
if ($id === null) {
return null;
}
return (new \objects\products_o())->select($id);
}, $data['products']);
$products = array_values(array_filter($products));
$criteria->products->set($products);
}
return $criteria;
}
/**
* @throws \Exception
*/
public static function calculateProgressFromArray(array $criteria_array): float
{
$criteria = self::fromJson(json_encode($criteria_array));
return $criteria->getProgress();
}
/**
* Export the criteria as an associative array suitable for JSON encoding.
* Ensures a canonical schema matching fromJson expectations.
* @return array
*/
public function toArray(): array
{
// Normalize lists to identifiers only
$users = $this->users?->listCustomerNumbers() ?? [];
$departments = $this->departments?->listIDs() ?? [];
$products = $this->products?->listIDs() ?? [];
// Remove nulls and duplicates just in case
$users = array_values(array_unique(array_filter($users, fn($v) => $v !== null)));
$departments = array_values(array_unique(array_filter($departments, fn($v) => $v !== null)));
$products = array_values(array_unique(array_filter($products, fn($v) => $v !== null)));
return [
'type' => $this->type?->name ?? goals_criteria_type::NONE->name,
'target' => $this->target ?? 0,
'label' => $this->label,
'start' => ($this->start instanceof \DateTimeInterface) ? $this->start->format(DATE_ATOM) : null,
'end' => ($this->end instanceof \DateTimeInterface) ? $this->end->format(DATE_ATOM) : null,
'users' => $users,
'departments' => $departments,
'products' => $products,
];
}
/**
* Export the criteria to JSON
* @return string
*/
public function toJson(): string
{
return json_encode($this->toArray());
}
private function getProgress(): int
{
return match ($this->type) {
goals_criteria_type::PRODUCT => $this->products->count($this),
goals_criteria_type::REVENUE => $this->sumRevenue(),
goals_criteria_type::VISITS => $this->countVisits(),
default => 0,
};
}
/**
* Calculate revenue within the criteria filters.
* Revenue is computed as sum(price * quantity) for matching order items.
* Prices are assumed to be stored as integers (e.g., cents) in the database.
*/
private function sumRevenue(): int
{
$datetime_start = $this->start ?? new \DateTime();
$datetime_end = $this->end ?? new \DateTime();
$items = order_items_o::getListByCriteria(
department_ids: $this->departments?->listIDs() ?? [],
product_ids: $this->products?->listIDs() ?? [],
customer_numbers: $this->users?->listCustomerNumbers() ?? [],
datetime_start: $datetime_start,
datetime_end: $datetime_end,
);
$sum = 0;
foreach ($items as $item) {
$price = (int)$item->price->value();
$qty = (int)$item->quantity->value();
$sum += $price * $qty;
}
return $sum;
}
/**
* Count visits within the criteria filters.
* In this domain a "visit" corresponds to unique order records.
*/
private function countVisits(): int
{
$datetime_start = $this->start ?? new \DateTime();
$datetime_end = $this->end ?? new \DateTime();
$items = order_items_o::getListByCriteria(
department_ids: $this->departments?->listIDs() ?? [],
product_ids: $this->products?->listIDs() ?? [],
customer_numbers: $this->users?->listCustomerNumbers() ?? [],
datetime_start: $datetime_start,
datetime_end: $datetime_end,
);
// Use an associative array to track unique order IDs
$unique_orders = [];
foreach ($items as $item) {
$order_id = (int)$item->order_id->value();
$unique_orders[$order_id] = true; // Value doesn't matter
}
return count($unique_orders);
}
/**
* Public accessor for the current result based on criteria type.
* Allows external callers (e.g., goals class) to retrieve the computed result
* without duplicating switch logic.
*/
public function currentResult(): int|float
{
return $this->getProgress();
}
}