Files
opensourcepos/app/Controllers/Secure_Controller.php
T
objecttothisandOllama 1b8ee2e3c1 feat(items): optimize search, attribute filtering, and sort for items view (#4652)
feat(items): optimize search, attribute filtering, and sort for items view (#4652)

BREAKING CHANGE: none

## Search & Query Optimization
- Split item search into two-phase query: Phase A resolves qualifying IDs,
  Phase B joins scoped display tables for better performance and readability
- Sanitize `definition_ids` via `array_map` to prevent injection vulnerabilities
- Introduce subquery for SUM aggregation to prevent over-counting across joins
- Add validation requiring both start and end dates before applying date range filter

## Attribute Search (fixes #2919, #2722)
- Add `SHOW_IN_SEARCH` flag (value 8) to Attribute model to separate
  searchability from table visibility
- Add `parse_attribute_search()` to parse syntax like `color:blue AND size:large`
- Add `applyNamedAttributeSearch()` supporting decimal and date types with
  locale-aware parsing
- Support AND/OR logic for multi-attribute queries

## Sorting
- Add `get_attribute_sort_definition_id()` to detect attribute column sorting
- Join attribute tables dynamically when sorting by attribute columns
- Use `MAX()` for consistent results when sorting by attribute values
- Replace static sort column list with dynamic headers via `itemSortColumns()`
- Add `sanitizeSortColumnAttribute()` to validate attribute definition IDs as sort columns

## Tax & Data Row
- Streamline tax computation in `getItemDataRow()`

## Low Inventory Filter
- Require valid `stock_location_id` before applying low inventory filter
- Add conditional logic to sort by sum of quantities across all locations
  when `stock_location_id` is invalid

## Localization
- Add `show_in_search` / `show_in_search_visibility` strings to all language files
- Translated: de-DE, es-ES, fr, it; English placeholder for remaining locales
- Unify single-quote style across all attribute language files

## Refactoring & Style
- Adopt camelCase naming throughout (variables, helpers, methods)
- Replace `sanitizeSortColumnAttribute` with reusable `sanitizeSortColumn`
  from `Secure_Controller`
- Simplify column key extraction using `array_key_first`
- Apply PSR-12 formatting

## Tests
- Add tests for tax computation, quantity aggregation (single- and multi-location),
  named attribute search, free-text parsing, and date/decimal type handling
- Add `ensureStockLocation` helper to auto-create missing stock locations in tests
- Refactor tests to handle config cache issues

Co-authored-by: Ollama <ollama@steganos.dev>
2026-10-02 14:56:20 +04:00

186 lines
5.0 KiB
PHP

<?php
namespace App\Controllers;
use App\Models\Employee;
use App\Models\Module;
use CodeIgniter\HTTP\Exceptions\RedirectException;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Model;
use CodeIgniter\Session\Session;
use Config\OSPOS;
use Config\Services;
/**
* Controllers that are considered secure extend Secure_Controller, optionally a $module_id can
* be set to also check if a user can access a particular module in the system.
*
* @property employee employee
* @property module module
* @property array global_view_data
* @property session session
*
*/
class Secure_Controller extends BaseController
{
public array $global_view_data;
protected Employee $employee;
protected Module $module;
protected Session $session;
/**
* @param string $module_id
* @param string|null $submodule_id
* @param string|null $menu_group
*/
public function __construct(string $module_id = '', ?string $submodule_id = null, ?string $menu_group = null)
{
$this->employee = model(Employee::class);
$this->module = model(Module::class);
$config = config(OSPOS::class)->settings;
$validation = Services::validation();
$logged_in_employee_info = $this->employee->get_logged_in_employee_info();
if (
!$this->employee->has_module_grant($module_id, $logged_in_employee_info->person_id)
|| (isset($submodule_id) && !$this->employee->has_module_grant($submodule_id, $logged_in_employee_info->person_id))
) {
throw new RedirectException("no_access/$module_id/$submodule_id");
}
// Load up global global_view_data visible to all the loaded views
$this->session = session();
if ($menu_group == null) {
$menu_group = $this->session->get('menu_group');
} else {
$this->session->set('menu_group', $menu_group);
}
$allowed_modules = $menu_group == 'home'
? $this->module->get_allowed_home_modules($logged_in_employee_info->person_id)
: $this->module->get_allowed_office_modules($logged_in_employee_info->person_id);
$this->global_view_data = [];
foreach ($allowed_modules->getResult() as $module) {
$this->global_view_data['allowed_modules'][] = $module;
}
$this->global_view_data += [
'user_info' => $logged_in_employee_info,
'controller_name' => $module_id,
'config' => $config
];
view('viewData', $this->global_view_data);
}
public function sanitizeSortColumn($headers, $field, $default): string
{
if ($field === null) {
return $default;
}
// Flatten keys directly as array_merge() renumbers numeric string keys
$validColumns = [];
foreach ($headers as $header) {
$validColumns[] = (string) array_key_first($header);
}
return in_array((string) $field, $validColumns, true) ? $field : $default;
}
/**
* Validates the given rules and, on failure, returns a JSON error response.
*
* @param array $rules
* @param array $messages
* @param mixed $id
* @return ResponseInterface|null
*/
protected function validateFields(array $rules, array $messages, $id = NEW_ENTRY): ?ResponseInterface
{
if (!$this->validate($rules, $messages)) {
$errors = $this->validator->getErrors();
return $this->response->setJSON(['success' => false, 'message' => reset($errors), 'id' => $id]);
}
return null;
}
/**
* AJAX function used to confirm whether values sent in the request are numeric
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function getCheckNumeric(): ResponseInterface
{
foreach ($this->request->getGet() as $value) {
if (parse_decimals($value) === false) {
return $this->response->setJSON('false');
}
}
return $this->response->setJSON('true');
}
/**
* @param $key
* @return mixed|void
*/
public function getConfig($key)
{
if (isset($config[$key])) {
return $config[$key];
}
}
/**
* @return false
*/
public function getIndex()
{
return false;
}
/**
* @return false
*/
public function getSearch()
{
return false;
}
/**
* @return false
*/
public function suggest_search()
{
return false;
}
/**
* @param int $data_item_id
* @return false
*/
public function getView(int $data_item_id = -1)
{
return false;
}
/**
* @param int $data_item_id
* @return ResponseInterface|false
*/
public function postSave(int $data_item_id = -1): ResponseInterface|false
{
return false;
}
/**
* @return false
*/
public function postDelete()
{
return false;
}
}