Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@
*/
class CustomersController
{
protected const DIRECT_FILTER_KEYS = [
'query',
];

protected modX $modx;

public function __construct(modX $modx)
{
$this->modx = $modx;
}

public static function getDirectFilterKeys(): array
{
return self::DIRECT_FILTER_KEYS;
}

/**
* Get list of customers with pagination and search
* GET /api/mgr/customers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
*/
class GridConfigController
{
private const DIRECT_FILTER_KEY_PROVIDERS = [
'orders' => OrdersController::class,
'customers' => CustomersController::class,
];

/** @var modX */
protected $modx;

Expand Down Expand Up @@ -49,14 +54,27 @@ public function getConfig(array $params): array
$includeHidden = !empty($params['include_hidden']);
$config = $this->service->getGridConfig($gridKey, $includeHidden);

$payload = ['columns' => $config];
$payload = [
'columns' => $config,
'direct_filter_keys' => $this->getDirectFilterKeys($gridKey),
];
if ($gridKey === 'category-products') {
$payload['editor_references'] = GridEditorReferenceRegistry::listForClient();
}

return Response::success($payload)->getData();
}

private function getDirectFilterKeys(string $gridKey): array
{
$provider = self::DIRECT_FILTER_KEY_PROVIDERS[$gridKey] ?? null;
if (!$provider || !method_exists($provider, 'getDirectFilterKeys')) {
return [];
}

return $provider::getDirectFilterKeys();
}

/**
* Save grid configuration
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,36 @@
*/
class OrdersController
{
protected const DIRECT_FILTER_KEYS = [
'query', // handled separately in getList(), not via applyDirectFilters
'status_id',
'delivery_id',
'payment_id',
'context_key',
'createdon_from', // date keys handled in applyDirectFilters(), not in FIELD_MAP
'createdon_to',
];

protected const DIRECT_FILTER_INT_KEYS = [
'status_id',
'delivery_id',
'payment_id',
];

/** Param key => msOrder column for direct (unprefixed) filter params. */
protected const DIRECT_FILTER_FIELD_MAP = [
'status_id' => 'status_id',
'delivery_id' => 'delivery_id',
'payment_id' => 'payment_id',
'context_key' => 'context',
];

protected const ADDRESS_FILTER_KEYS = [
'customer',
'email',
'phone',
];

protected modX $modx;
protected ?OrderLogService $orderLog = null;
protected ?Utils $ms3Utils = null;
Expand All @@ -52,6 +82,11 @@ public function __construct(modX $modx)
$this->loadExtraFieldsMap();
}

public static function getDirectFilterKeys(): array
{
return self::DIRECT_FILTER_KEYS;
}

/**
* Get OrderLogService (lazy loading from DI)
*
Expand Down Expand Up @@ -183,31 +218,7 @@ public function getList(array $params = []): array
}
}

// Direct filter params (from filter config)
if ($statusId = ($params['status_id'] ?? null)) {
$c->where(['status_id' => (int)$statusId]);
}
if ($deliveryId = ($params['delivery_id'] ?? null)) {
$c->where(['delivery_id' => (int)$deliveryId]);
}
if ($paymentId = ($params['payment_id'] ?? null)) {
$c->where(['payment_id' => (int)$paymentId]);
}
if ($contextKey = ($params['context_key'] ?? null)) {
$c->where(['context' => $contextKey]);
}

// Date range filters
if ($dateFrom = ($params['createdon_from'] ?? $params['date_start'] ?? null)) {
$c->where([
'msOrder.createdon:>=' => date('Y-m-d 00:00:00', strtotime($dateFrom)),
]);
}
if ($dateTo = ($params['createdon_to'] ?? $params['date_end'] ?? null)) {
$c->where([
'msOrder.createdon:<=' => date('Y-m-d 23:59:59', strtotime($dateTo)),
]);
}
$this->applyDirectFilters($c, $params);

// Legacy params for backward compatibility
if ($customer = ($params['customer'] ?? null)) {
Expand Down Expand Up @@ -1492,6 +1503,10 @@ protected function getOrdersStats(array $params = []): array
{
$c = $this->modx->newQuery(msOrder::class);

if ($this->hasAddressFilter($params)) {
$c->leftJoin(msOrderAddress::class, 'Address', '`Address`.order_id = msOrder.id');
}

// Filter by statuses for statistics (ms3_status_for_stat)
// Only count orders with these statuses (e.g. paid, completed)
$statusForStat = $this->modx->getOption('ms3_status_for_stat', null, '2,3');
Expand All @@ -1510,31 +1525,7 @@ protected function getOrdersStats(array $params = []): array
}
}

// Direct filter params
if ($statusId = ($params['status_id'] ?? null)) {
$c->where(['status_id' => (int)$statusId]);
}
if ($deliveryId = ($params['delivery_id'] ?? null)) {
$c->where(['delivery_id' => (int)$deliveryId]);
}
if ($paymentId = ($params['payment_id'] ?? null)) {
$c->where(['payment_id' => (int)$paymentId]);
}
if ($contextKey = ($params['context_key'] ?? null)) {
$c->where(['context' => $contextKey]);
}

// Date range filters
if ($dateFrom = ($params['createdon_from'] ?? $params['date_start'] ?? null)) {
$c->where([
'msOrder.createdon:>=' => date('Y-m-d 00:00:00', strtotime($dateFrom)),
]);
}
if ($dateTo = ($params['createdon_to'] ?? $params['date_end'] ?? null)) {
$c->where([
'msOrder.createdon:<=' => date('Y-m-d 23:59:59', strtotime($dateTo)),
]);
}
$this->applyDirectFilters($c, $params);

// Calculate sum and count
$c->select('SUM(msOrder.cost) as sum, COUNT(msOrder.id) as total');
Expand All @@ -1548,6 +1539,18 @@ protected function getOrdersStats(array $params = []): array
];
}

protected function hasAddressFilter(array $params): bool
{
foreach (self::ADDRESS_FILTER_KEYS as $fieldName) {
$value = $params['filter_' . $fieldName] ?? null;
if ($value !== null && $value !== '') {
return true;
}
}

return false;
}

/**
* Whether draft orders should be included in the manager orders list query.
*
Expand Down Expand Up @@ -1630,6 +1633,42 @@ protected function applyFilter($c, string $fieldName, $value): void
}
}

/**
* Apply direct (unprefixed) filter params to an orders query.
*
* @param \xPDO\Om\xPDOQuery $c Query object
* @param array $params Request parameters
*/
protected function applyDirectFilters($c, array $params): void
{
foreach (self::DIRECT_FILTER_FIELD_MAP as $paramKey => $fieldName) {
$value = $params[$paramKey] ?? null;
if ($value === null || $value === '') {
continue;
}

if (in_array($paramKey, self::DIRECT_FILTER_INT_KEYS, true)) {
$value = (int)$value;
}

$c->where([$fieldName => $value]);
}

$dateFrom = $params['createdon_from'] ?? $params['date_start'] ?? null;
if ($dateFrom) {
$c->where([
'msOrder.createdon:>=' => date('Y-m-d 00:00:00', strtotime($dateFrom)),
]);
}

$dateTo = $params['createdon_to'] ?? $params['date_end'] ?? null;
if ($dateTo) {
$c->where([
'msOrder.createdon:<=' => date('Y-m-d 23:59:59', strtotime($dateTo)),
]);
}
}

/**
* Map sort field to database column
*
Expand Down
71 changes: 71 additions & 0 deletions core/components/minishop3/tests/DirectFilterKeysTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/**
* Static checks for manager grid direct filter keys (without MODX).
*
* Run: php tests/DirectFilterKeysTest.php
*/

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use MiniShop3\Controllers\Api\Manager\CustomersController;
use MiniShop3\Controllers\Api\Manager\GridConfigController;
use MiniShop3\Controllers\Api\Manager\OrdersController;

$fail = static function (string $message): never {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
};

$assertSame = static function (array $expected, array $actual, string $label) use ($fail): void {
if ($actual !== $expected) {
$fail(sprintf(
"%s:\nexpected: %s\nactual: %s",
$label,
json_encode($expected, JSON_UNESCAPED_SLASHES),
json_encode($actual, JSON_UNESCAPED_SLASHES)
));
}
};

$assertKeysInList = static function (array $keys, array $list, string $label) use ($fail): void {
foreach ($keys as $key) {
if (!in_array($key, $list, true)) {
$fail("{$label}: key {$key} missing from DIRECT_FILTER_KEYS");
}
}
};

$ordersReflection = new ReflectionClass(OrdersController::class);
$ordersKeys = $ordersReflection->getConstant('DIRECT_FILTER_KEYS');
$ordersFieldMap = $ordersReflection->getConstant('DIRECT_FILTER_FIELD_MAP');

$assertSame($ordersKeys, OrdersController::getDirectFilterKeys(), 'orders getter returns DIRECT_FILTER_KEYS');
$assertKeysInList(array_keys($ordersFieldMap), $ordersKeys, 'orders FIELD_MAP');

foreach (['query', 'createdon_from', 'createdon_to'] as $separateKey) {
if (array_key_exists($separateKey, $ordersFieldMap)) {
$fail("orders key {$separateKey} must not be in DIRECT_FILTER_FIELD_MAP");
}
}

$customersReflection = new ReflectionClass(CustomersController::class);
$assertSame(
$customersReflection->getConstant('DIRECT_FILTER_KEYS'),
CustomersController::getDirectFilterKeys(),
'customers getter returns DIRECT_FILTER_KEYS'
);

$gridConfig = new ReflectionClass(GridConfigController::class);
$providers = $gridConfig->getConstant('DIRECT_FILTER_KEY_PROVIDERS');

foreach (['orders' => OrdersController::class, 'customers' => CustomersController::class] as $gridKey => $provider) {
if (($providers[$gridKey] ?? null) !== $provider) {
$fail("grid-config provider missing for {$gridKey}");
}
}

fwrite(STDOUT, "OK DirectFilterKeysTest\n");
exit(0);
6 changes: 5 additions & 1 deletion vueManager/src/components/CustomersGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useConfirm } from 'primevue/useconfirm'
import { useToast } from 'primevue/usetoast'
import { computed, nextTick, onMounted, ref } from 'vue'

import { useGridFilterParams } from '../composables/useGridFilterParams.js'
import { useSelection } from '../composables/useSelection.js'
import request from '../request.js'
import ActionsColumn from './ActionsColumn.vue'
Expand All @@ -42,6 +43,7 @@ const {
})

const columns = ref([])
const { setDirectFilterKeys, addFilterParam } = useGridFilterParams()
const loading = ref(false)
const customers = ref([])
const totalRecords = ref(0)
Expand Down Expand Up @@ -99,7 +101,7 @@ async function loadCustomers() {
Object.keys(filterValues.value).forEach(key => {
const value = filterValues.value[key]
if (value !== null && value !== undefined && value !== '') {
params[`filter_${key}`] = value
addFilterParam(params, key, value)
}
})

Expand Down Expand Up @@ -506,10 +508,12 @@ async function loadGridConfig() {
try {
const response = await request.get('/api/mgr/grid-config/customers')
columns.value = response.columns || []
setDirectFilterKeys(response.direct_filter_keys)
initFilters()
} catch (error) {
console.error('[CustomersGrid] Failed to load grid config:', error)
columns.value = getDefaultColumns()
setDirectFilterKeys([])
initFilters()
}
}
Expand Down
Loading