ver. 0.274 - ShopClients Domain+DI migration
This commit is contained in:
@@ -1,48 +1,9 @@
|
|||||||
<div class="site-title"><?php echo $this->name . ' ' . $this->surname; ?></div>
|
<div class="site-title"><?= htmlspecialchars($this->name . ' ' . $this->surname, ENT_QUOTES, 'UTF-8'); ?></div>
|
||||||
<div class="site-subtitle">Łączne zakupy w wysokości: <?php echo $this->total_spent; ?> zł</div>
|
<div class="site-subtitle">
|
||||||
|
Łączne zakupy: <?= number_format((float)$this->total_spent, 2, '.', ' '); ?> zl,
|
||||||
|
liczba zamówień: <?= (int)$this->total_orders; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br />
|
||||||
|
|
||||||
<?
|
<?= \Tpl::view('components/table-list', ['list' => $this->ordersTable]); ?>
|
||||||
global $gdb;
|
|
||||||
$grid = new \grid( 'pp_shop_orders' );
|
|
||||||
$grid -> gdb_opt = $gdb;
|
|
||||||
|
|
||||||
$grid->src = $this->orders_info;
|
|
||||||
|
|
||||||
$grid -> debug = true;
|
|
||||||
$grid -> order = [ 'column' => 'date_order', 'type' => 'DESC' ];
|
|
||||||
$grid -> search = [];
|
|
||||||
$grid -> columns_view = [
|
|
||||||
[
|
|
||||||
'name' => 'Lp.',
|
|
||||||
'th' => [ 'class' => 'g-lp' ],
|
|
||||||
'td' => [ 'class' => 'g-center' ],
|
|
||||||
'autoincrement' => true
|
|
||||||
], [
|
|
||||||
'name' => 'Data zamówienia',
|
|
||||||
'db' => 'date_order',
|
|
||||||
'td' => [ 'class' => 'g-center' ],
|
|
||||||
'th' => [ 'class' => 'g-center', 'style' => 'width: 175px;' ],
|
|
||||||
], [
|
|
||||||
'name' => 'Wartość',
|
|
||||||
'db' => 'summary',
|
|
||||||
'td' => [ 'class' => 'g-right' ],
|
|
||||||
'th' => [ 'class' => 'g-right', 'style' => 'width: 150px;' ],
|
|
||||||
'php' => 'echo number_format( "[summary]", 2, ".", " " ) . " zł";',
|
|
||||||
], [
|
|
||||||
'name' => 'Typ płatności',
|
|
||||||
'db' => 'payment_method',
|
|
||||||
'th' => [ 'style' => 'width: 350px;' ],
|
|
||||||
], [
|
|
||||||
'name' => 'Rodzaj transportu',
|
|
||||||
'db' => 'transport',
|
|
||||||
'th' => [ 'style' => 'width: 350px;' ],
|
|
||||||
], [
|
|
||||||
'name' => 'Wiadomość',
|
|
||||||
'db' => 'message',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
echo $grid -> draw();
|
|
||||||
?>
|
|
||||||
@@ -1,84 +1,2 @@
|
|||||||
<div class="site-title">Lista klientów</div>
|
<div class="site-title">Lista klientów</div>
|
||||||
<?php
|
<?= \Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
||||||
global $gdb;
|
|
||||||
|
|
||||||
$grid = new \grid( '__shop_clients' );
|
|
||||||
$grid -> gdb_opt = $gdb;
|
|
||||||
|
|
||||||
$grid->sql = "
|
|
||||||
SELECT
|
|
||||||
MAX( client_id ) AS client_id, client_name, client_surname, client_email, MAX( client_phone ) AS client_phone, MAX( client_city ) AS client_city, COUNT(*) AS total_orders, SUM(summary) AS total_spent,
|
|
||||||
CASE
|
|
||||||
WHEN MAX(client_id) IS NOT NULL
|
|
||||||
THEN 'Zarejestrowany'
|
|
||||||
ELSE
|
|
||||||
'Gość'
|
|
||||||
END AS client_type
|
|
||||||
FROM pp_shop_orders
|
|
||||||
WHERE client_name IS NOT NULL AND client_surname IS NOT NULL AND client_email IS NOT NULL
|
|
||||||
GROUP BY client_name, client_surname, client_email
|
|
||||||
ORDER BY
|
|
||||||
CASE
|
|
||||||
WHEN MAX(client_id) IS NOT NULL THEN 1
|
|
||||||
ELSE 2
|
|
||||||
END,
|
|
||||||
client_name ASC";
|
|
||||||
$grid -> sql_count = "
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM ( SELECT client_name, client_surname, client_email
|
|
||||||
FROM pp_shop_orders
|
|
||||||
WHERE client_name IS NOT NULL AND client_surname IS NOT NULL AND client_email IS NOT NULL
|
|
||||||
GROUP BY client_name, client_surname, client_email
|
|
||||||
) AS unique_clients";
|
|
||||||
|
|
||||||
$grid -> order = [ 'column' => 'date_order', 'type' => 'DESC' ];
|
|
||||||
$grid -> search = [];
|
|
||||||
$grid -> columns_view = [
|
|
||||||
[
|
|
||||||
'name' => 'Lp.',
|
|
||||||
'th' => [ 'class' => 'g-lp' ],
|
|
||||||
'td' => [ 'class' => 'g-center' ],
|
|
||||||
'autoincrement' => true
|
|
||||||
], [
|
|
||||||
'name' => 'Typ klienta',
|
|
||||||
'db' => 'client_type',
|
|
||||||
'td' => [ 'class' => 'g-center' ],
|
|
||||||
'th' => [ 'class' => 'g-center', 'style' => 'width: 100px;' ],
|
|
||||||
], [
|
|
||||||
'name' => 'Nazwisko, imię',
|
|
||||||
'th' => [ 'style' => 'width: 225px;' ],
|
|
||||||
'php' => 'echo "[client_surname]" . " " . "[client_name]";',
|
|
||||||
], [
|
|
||||||
'name' => 'Email',
|
|
||||||
'db' => 'client_email',
|
|
||||||
'th' => [ 'style' => 'width: 150px;' ],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Telefon',
|
|
||||||
'db' => 'client_phone',
|
|
||||||
'th' => [ 'style' => 'width: 150px;' ],
|
|
||||||
], [
|
|
||||||
'name' => 'Miasto',
|
|
||||||
'db' => 'client_city',
|
|
||||||
'th' => [ 'style' => 'width: 150px;' ],
|
|
||||||
], [
|
|
||||||
'name' => 'Wartość zamówień',
|
|
||||||
'db' => 'total_spent',
|
|
||||||
'td' => [ 'class' => 'g-right' ],
|
|
||||||
'th' => [ 'class' => 'g-right', 'style' => 'width: 150px;' ],
|
|
||||||
'php' => 'echo number_format( "[total_spent]", 2, ".", " " ) . " zł";',
|
|
||||||
], [
|
|
||||||
'name' => 'Ilosc zamówień',
|
|
||||||
'db' => 'total_orders',
|
|
||||||
'td' => [ 'class' => 'g-center' ],
|
|
||||||
'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ],
|
|
||||||
'php' => 'echo "<a href=\'/admin/shop_clients/clients_details/?name=[client_name]&surname=[client_surname]&email=[client_email]&total_spent=[total_spent]\'>[total_orders]</a>";'
|
|
||||||
], [
|
|
||||||
'name' => 'Akcje',
|
|
||||||
'th' => [ 'class' => 'g-center', 'style' => 'width: 100px;' ],
|
|
||||||
'td' => [ 'class' => 'g-center' ],
|
|
||||||
'php' => 'echo "<a href=\'/admin/shop_clients/clients_details/?name=[client_name]&surname=[client_surname]&email=[client_email]&total_spent=[total_spent]\'>zobacz zamówienia</a>";',
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
echo $grid -> draw();
|
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li> <a href="/admin/shop_order/view_list/"><img src="/admin/layout/icon/icon-menu/shopping-cart.svg">Zamówienia</a></li>
|
<li> <a href="/admin/shop_order/view_list/"><img src="/admin/layout/icon/icon-menu/shopping-cart.svg">Zamówienia</a></li>
|
||||||
<li> <a href="/admin/shop_clients/view_list/"><img src="/admin/layout/icon/icon-menu/people-fill.svg">Klienci</a></li>
|
<li> <a href="/admin/shop_clients/list/"><img src="/admin/layout/icon/icon-menu/people-fill.svg">Klienci</a></li>
|
||||||
<li><a href="/admin/shop_category/view_list/"><img src="/admin/layout/icon/icon-menu/bxs-category-alt.svg">Kategorie</a></li>
|
<li><a href="/admin/shop_category/view_list/"><img src="/admin/layout/icon/icon-menu/bxs-category-alt.svg">Kategorie</a></li>
|
||||||
<li><a href="/admin/shop_product/view_list/"><img src="/admin/layout/icon/icon-menu/shopping-basket.svg">Produkty</a></li>
|
<li><a href="/admin/shop_product/view_list/"><img src="/admin/layout/icon/icon-menu/shopping-basket.svg">Produkty</a></li>
|
||||||
<li><a href="/admin/shop_product/mass_edit/"><i class="fa fa-bars"></i>Masowa edycja</a></li>
|
<li><a href="/admin/shop_product/mass_edit/"><i class="fa fa-bars"></i>Masowa edycja</a></li>
|
||||||
|
|||||||
250
autoload/Domain/Client/ClientRepository.php
Normal file
250
autoload/Domain/Client/ClientRepository.php
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<?php
|
||||||
|
namespace Domain\Client;
|
||||||
|
|
||||||
|
class ClientRepository
|
||||||
|
{
|
||||||
|
private const MAX_PER_PAGE = 100;
|
||||||
|
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct($db)
|
||||||
|
{
|
||||||
|
$this->db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{items: array<int, array<string, mixed>>, total: int}
|
||||||
|
*/
|
||||||
|
public function listForAdmin(
|
||||||
|
array $filters,
|
||||||
|
string $sortColumn = 'client_surname',
|
||||||
|
string $sortDir = 'ASC',
|
||||||
|
int $page = 1,
|
||||||
|
int $perPage = 15
|
||||||
|
): array {
|
||||||
|
$allowedSortColumns = [
|
||||||
|
'client_name' => 'c.client_name',
|
||||||
|
'client_surname' => 'c.client_surname',
|
||||||
|
'client_email' => 'c.client_email',
|
||||||
|
'client_phone' => 'c.client_phone',
|
||||||
|
'client_city' => 'c.client_city',
|
||||||
|
'total_orders' => 'c.total_orders',
|
||||||
|
'total_spent' => 'c.total_spent',
|
||||||
|
'client_type' => 'c.is_registered',
|
||||||
|
];
|
||||||
|
|
||||||
|
$sortSql = $allowedSortColumns[$sortColumn] ?? 'c.client_surname';
|
||||||
|
$sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC';
|
||||||
|
$page = max(1, $page);
|
||||||
|
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
|
||||||
|
$offset = ($page - 1) * $perPage;
|
||||||
|
|
||||||
|
$where = [
|
||||||
|
'o.client_name IS NOT NULL',
|
||||||
|
'o.client_surname IS NOT NULL',
|
||||||
|
'o.client_email IS NOT NULL',
|
||||||
|
];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
$name = $this->normalizeTextFilter($filters['name'] ?? '');
|
||||||
|
if ($name !== '') {
|
||||||
|
$where[] = 'o.client_name LIKE :name';
|
||||||
|
$params[':name'] = '%' . $name . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
$surname = $this->normalizeTextFilter($filters['surname'] ?? '');
|
||||||
|
if ($surname !== '') {
|
||||||
|
$where[] = 'o.client_surname LIKE :surname';
|
||||||
|
$params[':surname'] = '%' . $surname . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = $this->normalizeTextFilter($filters['email'] ?? '');
|
||||||
|
if ($email !== '') {
|
||||||
|
$where[] = 'o.client_email LIKE :email';
|
||||||
|
$params[':email'] = '%' . $email . '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientType = trim((string)($filters['client_type'] ?? ''));
|
||||||
|
if ($clientType === 'registered') {
|
||||||
|
$where[] = 'o.client_id IS NOT NULL';
|
||||||
|
} elseif ($clientType === 'guest') {
|
||||||
|
$where[] = 'o.client_id IS NULL';
|
||||||
|
}
|
||||||
|
|
||||||
|
$whereSql = implode(' AND ', $where);
|
||||||
|
|
||||||
|
$aggregatedSql = "
|
||||||
|
SELECT
|
||||||
|
MAX(o.client_id) AS client_id,
|
||||||
|
o.client_name,
|
||||||
|
o.client_surname,
|
||||||
|
o.client_email,
|
||||||
|
MAX(o.client_phone) AS client_phone,
|
||||||
|
MAX(o.client_city) AS client_city,
|
||||||
|
COUNT(*) AS total_orders,
|
||||||
|
COALESCE(SUM(o.summary), 0) AS total_spent,
|
||||||
|
CASE
|
||||||
|
WHEN MAX(o.client_id) IS NOT NULL THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS is_registered
|
||||||
|
FROM pp_shop_orders AS o
|
||||||
|
WHERE {$whereSql}
|
||||||
|
GROUP BY o.client_name, o.client_surname, o.client_email
|
||||||
|
";
|
||||||
|
|
||||||
|
$sqlCount = "
|
||||||
|
SELECT COUNT(0)
|
||||||
|
FROM ({$aggregatedSql}) AS c
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmtCount = $this->db->query($sqlCount, $params);
|
||||||
|
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
|
||||||
|
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
c.client_id,
|
||||||
|
c.client_name,
|
||||||
|
c.client_surname,
|
||||||
|
c.client_email,
|
||||||
|
c.client_phone,
|
||||||
|
c.client_city,
|
||||||
|
c.total_orders,
|
||||||
|
c.total_spent,
|
||||||
|
c.is_registered
|
||||||
|
FROM ({$aggregatedSql}) AS c
|
||||||
|
ORDER BY {$sortSql} {$sortDir}, c.client_surname ASC, c.client_name ASC
|
||||||
|
LIMIT {$perPage} OFFSET {$offset}
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $this->db->query($sql, $params);
|
||||||
|
$items = $stmt ? $stmt->fetchAll() : [];
|
||||||
|
|
||||||
|
if (!is_array($items)) {
|
||||||
|
$items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($items as &$item) {
|
||||||
|
$item['client_id'] = !isset($item['client_id']) ? null : (int)$item['client_id'];
|
||||||
|
$item['client_name'] = (string)($item['client_name'] ?? '');
|
||||||
|
$item['client_surname'] = (string)($item['client_surname'] ?? '');
|
||||||
|
$item['client_email'] = (string)($item['client_email'] ?? '');
|
||||||
|
$item['client_phone'] = (string)($item['client_phone'] ?? '');
|
||||||
|
$item['client_city'] = (string)($item['client_city'] ?? '');
|
||||||
|
$item['total_orders'] = (int)($item['total_orders'] ?? 0);
|
||||||
|
$item['total_spent'] = (float)($item['total_spent'] ?? 0);
|
||||||
|
$item['is_registered'] = ((int)($item['is_registered'] ?? 0)) === 1 ? 1 : 0;
|
||||||
|
}
|
||||||
|
unset($item);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $items,
|
||||||
|
'total' => $total,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function ordersForClient(string $name, string $surname, string $email): array
|
||||||
|
{
|
||||||
|
$name = trim($name);
|
||||||
|
$surname = trim($surname);
|
||||||
|
$email = trim($email);
|
||||||
|
|
||||||
|
if ($name === '' || $surname === '' || $email === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
o.id,
|
||||||
|
o.date_order,
|
||||||
|
o.summary,
|
||||||
|
o.payment_method,
|
||||||
|
o.transport,
|
||||||
|
o.message
|
||||||
|
FROM pp_shop_orders AS o
|
||||||
|
WHERE o.client_name = :name
|
||||||
|
AND o.client_surname = :surname
|
||||||
|
AND o.client_email = :email
|
||||||
|
ORDER BY o.date_order DESC, o.id DESC
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $this->db->query($sql, [
|
||||||
|
':name' => $name,
|
||||||
|
':surname' => $surname,
|
||||||
|
':email' => $email,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rows = $stmt ? $stmt->fetchAll() : [];
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rows as &$row) {
|
||||||
|
$row['id'] = (int)($row['id'] ?? 0);
|
||||||
|
$row['date_order'] = (string)($row['date_order'] ?? '');
|
||||||
|
$row['summary'] = (float)($row['summary'] ?? 0);
|
||||||
|
$row['payment_method'] = (string)($row['payment_method'] ?? '');
|
||||||
|
$row['transport'] = (string)($row['transport'] ?? '');
|
||||||
|
$row['message'] = (string)($row['message'] ?? '');
|
||||||
|
}
|
||||||
|
unset($row);
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{total_orders: int, total_spent: float}
|
||||||
|
*/
|
||||||
|
public function totalsForClient(string $name, string $surname, string $email): array
|
||||||
|
{
|
||||||
|
$name = trim($name);
|
||||||
|
$surname = trim($surname);
|
||||||
|
$email = trim($email);
|
||||||
|
|
||||||
|
if ($name === '' || $surname === '' || $email === '') {
|
||||||
|
return [
|
||||||
|
'total_orders' => 0,
|
||||||
|
'total_spent' => 0.0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS total_orders,
|
||||||
|
COALESCE(SUM(o.summary), 0) AS total_spent
|
||||||
|
FROM pp_shop_orders AS o
|
||||||
|
WHERE o.client_name = :name
|
||||||
|
AND o.client_surname = :surname
|
||||||
|
AND o.client_email = :email
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $this->db->query($sql, [
|
||||||
|
':name' => $name,
|
||||||
|
':surname' => $surname,
|
||||||
|
':email' => $email,
|
||||||
|
]);
|
||||||
|
$rows = $stmt ? $stmt->fetchAll() : [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total_orders' => isset($rows[0]['total_orders']) ? (int)$rows[0]['total_orders'] : 0,
|
||||||
|
'total_spent' => isset($rows[0]['total_spent']) ? (float)$rows[0]['total_spent'] : 0.0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeTextFilter($value): string
|
||||||
|
{
|
||||||
|
$value = trim((string)$value);
|
||||||
|
if ($value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen($value) > 255) {
|
||||||
|
return substr($value, 0, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
222
autoload/admin/Controllers/ShopClientsController.php
Normal file
222
autoload/admin/Controllers/ShopClientsController.php
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\Controllers;
|
||||||
|
|
||||||
|
use Domain\Client\ClientRepository;
|
||||||
|
use admin\ViewModels\Common\PaginatedTableViewModel;
|
||||||
|
|
||||||
|
class ShopClientsController
|
||||||
|
{
|
||||||
|
private ClientRepository $repository;
|
||||||
|
|
||||||
|
public function __construct(ClientRepository $repository)
|
||||||
|
{
|
||||||
|
$this->repository = $repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function list(): string
|
||||||
|
{
|
||||||
|
$sortableColumns = [
|
||||||
|
'client_name',
|
||||||
|
'client_surname',
|
||||||
|
'client_email',
|
||||||
|
'client_phone',
|
||||||
|
'client_city',
|
||||||
|
'total_orders',
|
||||||
|
'total_spent',
|
||||||
|
'client_type',
|
||||||
|
];
|
||||||
|
|
||||||
|
$filterDefinitions = [
|
||||||
|
[
|
||||||
|
'key' => 'name',
|
||||||
|
'label' => 'Imie',
|
||||||
|
'type' => 'text',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'surname',
|
||||||
|
'label' => 'Nazwisko',
|
||||||
|
'type' => 'text',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'email',
|
||||||
|
'label' => 'E-mail',
|
||||||
|
'type' => 'text',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'key' => 'client_type',
|
||||||
|
'label' => 'Typ klienta',
|
||||||
|
'type' => 'select',
|
||||||
|
'options' => [
|
||||||
|
'' => '- typ klienta -',
|
||||||
|
'registered' => 'Zarejestrowany',
|
||||||
|
'guest' => 'Gosc',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$listRequest = \admin\Support\TableListRequestFactory::fromRequest(
|
||||||
|
$filterDefinitions,
|
||||||
|
$sortableColumns,
|
||||||
|
'client_surname'
|
||||||
|
);
|
||||||
|
|
||||||
|
$sortDir = $listRequest['sortDir'];
|
||||||
|
if (trim((string)\S::get('sort')) === '') {
|
||||||
|
$sortDir = 'ASC';
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->repository->listForAdmin(
|
||||||
|
$listRequest['filters'],
|
||||||
|
$listRequest['sortColumn'],
|
||||||
|
$sortDir,
|
||||||
|
$listRequest['page'],
|
||||||
|
$listRequest['perPage']
|
||||||
|
);
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1;
|
||||||
|
|
||||||
|
foreach ($result['items'] as $item) {
|
||||||
|
$name = trim((string)($item['client_name'] ?? ''));
|
||||||
|
$surname = trim((string)($item['client_surname'] ?? ''));
|
||||||
|
$email = trim((string)($item['client_email'] ?? ''));
|
||||||
|
$params = [
|
||||||
|
'name' => $name,
|
||||||
|
'surname' => $surname,
|
||||||
|
'email' => $email,
|
||||||
|
];
|
||||||
|
$detailsUrl = '/admin/shop_clients/details/?' . http_build_query($params);
|
||||||
|
|
||||||
|
$rows[] = [
|
||||||
|
'lp' => $lp++ . '.',
|
||||||
|
'client_type' => ((int)($item['is_registered'] ?? 0) === 1) ? 'Zarejestrowany' : 'Gosc',
|
||||||
|
'full_name' => htmlspecialchars($surname, ENT_QUOTES, 'UTF-8') . ' ' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8'),
|
||||||
|
'client_email' => $email,
|
||||||
|
'client_phone' => (string)($item['client_phone'] ?? ''),
|
||||||
|
'client_city' => (string)($item['client_city'] ?? ''),
|
||||||
|
'total_spent' => number_format((float)($item['total_spent'] ?? 0), 2, '.', ' ') . ' zl',
|
||||||
|
'total_orders' => '<a href="' . htmlspecialchars($detailsUrl, ENT_QUOTES, 'UTF-8') . '">' . (int)($item['total_orders'] ?? 0) . '</a>',
|
||||||
|
'_actions' => [
|
||||||
|
[
|
||||||
|
'label' => 'Zobacz zamowienia',
|
||||||
|
'url' => $detailsUrl,
|
||||||
|
'class' => 'btn btn-xs btn-primary',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = (int)$result['total'];
|
||||||
|
$totalPages = max(1, (int)ceil($total / $listRequest['perPage']));
|
||||||
|
|
||||||
|
$viewModel = new PaginatedTableViewModel(
|
||||||
|
[
|
||||||
|
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
||||||
|
['key' => 'client_type', 'sort_key' => 'client_type', 'label' => 'Typ klienta', 'class' => 'text-center', 'sortable' => true],
|
||||||
|
['key' => 'full_name', 'label' => 'Nazwisko, imie', 'sortable' => false, 'raw' => true],
|
||||||
|
['key' => 'client_email', 'sort_key' => 'client_email', 'label' => 'Email', 'sortable' => true],
|
||||||
|
['key' => 'client_phone', 'sort_key' => 'client_phone', 'label' => 'Telefon', 'sortable' => true],
|
||||||
|
['key' => 'client_city', 'sort_key' => 'client_city', 'label' => 'Miasto', 'sortable' => true],
|
||||||
|
['key' => 'total_spent', 'sort_key' => 'total_spent', 'label' => 'Wartosc zamowien', 'class' => 'text-right', 'sortable' => true],
|
||||||
|
['key' => 'total_orders', 'sort_key' => 'total_orders', 'label' => 'Ilosc zamowien', 'class' => 'text-center', 'sortable' => true, 'raw' => true],
|
||||||
|
],
|
||||||
|
$rows,
|
||||||
|
$listRequest['viewFilters'],
|
||||||
|
[
|
||||||
|
'column' => $listRequest['sortColumn'],
|
||||||
|
'dir' => $sortDir,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'page' => $listRequest['page'],
|
||||||
|
'per_page' => $listRequest['perPage'],
|
||||||
|
'total' => $total,
|
||||||
|
'total_pages' => $totalPages,
|
||||||
|
],
|
||||||
|
array_merge($listRequest['queryFilters'], [
|
||||||
|
'sort' => $listRequest['sortColumn'],
|
||||||
|
'dir' => $sortDir,
|
||||||
|
'per_page' => $listRequest['perPage'],
|
||||||
|
]),
|
||||||
|
$listRequest['perPageOptions'],
|
||||||
|
$sortableColumns,
|
||||||
|
'/admin/shop_clients/list/',
|
||||||
|
'Brak danych w tabeli.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return \Tpl::view('shop-clients/view-list', [
|
||||||
|
'viewModel' => $viewModel,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view_list(): string
|
||||||
|
{
|
||||||
|
return $this->list();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function details(): string
|
||||||
|
{
|
||||||
|
$name = (string)\S::get('name');
|
||||||
|
$surname = (string)\S::get('surname');
|
||||||
|
$email = (string)\S::get('email');
|
||||||
|
|
||||||
|
$ordersInfo = $this->repository->ordersForClient($name, $surname, $email);
|
||||||
|
$totals = $this->repository->totalsForClient($name, $surname, $email);
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$lp = 1;
|
||||||
|
foreach ($ordersInfo as $order) {
|
||||||
|
$rows[] = [
|
||||||
|
'lp' => $lp++ . '.',
|
||||||
|
'date_order' => (string)($order['date_order'] ?? ''),
|
||||||
|
'summary' => number_format((float)($order['summary'] ?? 0), 2, '.', ' ') . ' zl',
|
||||||
|
'payment_method' => (string)($order['payment_method'] ?? ''),
|
||||||
|
'transport' => (string)($order['transport'] ?? ''),
|
||||||
|
'message' => (string)($order['message'] ?? ''),
|
||||||
|
'_actions' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ordersTable = new PaginatedTableViewModel(
|
||||||
|
[
|
||||||
|
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
||||||
|
['key' => 'date_order', 'label' => 'Data zamowienia', 'class' => 'text-center', 'sortable' => false],
|
||||||
|
['key' => 'summary', 'label' => 'Wartosc', 'class' => 'text-right', 'sortable' => false],
|
||||||
|
['key' => 'payment_method', 'label' => 'Typ platnosci', 'sortable' => false],
|
||||||
|
['key' => 'transport', 'label' => 'Rodzaj transportu', 'sortable' => false],
|
||||||
|
['key' => 'message', 'label' => 'Wiadomosc', 'sortable' => false],
|
||||||
|
],
|
||||||
|
$rows,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
'page' => 1,
|
||||||
|
'per_page' => max(1, count($rows)),
|
||||||
|
'total' => count($rows),
|
||||||
|
'total_pages' => 1,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
[count($rows) > 0 ? count($rows) : 1],
|
||||||
|
[],
|
||||||
|
'/admin/shop_clients/details/?' . http_build_query([
|
||||||
|
'name' => $name,
|
||||||
|
'surname' => $surname,
|
||||||
|
'email' => $email,
|
||||||
|
]),
|
||||||
|
'Brak zamowien klienta.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return \Tpl::view('shop-clients/clients-details', [
|
||||||
|
'name' => $name,
|
||||||
|
'surname' => $surname,
|
||||||
|
'email' => $email,
|
||||||
|
'total_spent' => $totals['total_spent'],
|
||||||
|
'ordersTable' => $ordersTable,
|
||||||
|
'total_orders' => $totals['total_orders'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clients_details(): string
|
||||||
|
{
|
||||||
|
return $this->details();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -384,6 +384,13 @@ class Site
|
|||||||
new \Domain\Product\ProductRepository( $mdb )
|
new \Domain\Product\ProductRepository( $mdb )
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
'ShopClients' => function() {
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
return new \admin\Controllers\ShopClientsController(
|
||||||
|
new \Domain\Client\ClientRepository( $mdb )
|
||||||
|
);
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return self::$newControllers;
|
return self::$newControllers;
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace admin\controls;
|
|
||||||
class ShopClients
|
|
||||||
{
|
|
||||||
public static function view_list()
|
|
||||||
{
|
|
||||||
return \Tpl::view(
|
|
||||||
'shop-clients/view-list'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function clients_details()
|
|
||||||
{
|
|
||||||
$query_string = $_SERVER['REDIRECT_QUERY_STRING'];
|
|
||||||
parse_str($query_string, $query_array);
|
|
||||||
|
|
||||||
$orders_info = \admin\factory\ShopClients::get_order_all_info( $query_array['name'], $query_array['surname'], $query_array['email'] );
|
|
||||||
|
|
||||||
return \Tpl::view('shop-clients/clients-details', [
|
|
||||||
'name' => $query_array['name'],
|
|
||||||
'surname' => $query_array['surname'],
|
|
||||||
'email' => $query_array['email'],
|
|
||||||
'total_spent' => $query_array['total_spent'],
|
|
||||||
'orders_info' => $orders_info
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace admin\factory;
|
|
||||||
class ShopClients
|
|
||||||
{
|
|
||||||
public static function get_order_all_info($name, $surname, $email)
|
|
||||||
{
|
|
||||||
global $mdb;
|
|
||||||
|
|
||||||
$results = $mdb->select('pp_shop_orders', '*', [
|
|
||||||
'client_name' => $name,
|
|
||||||
'client_surname' => $surname,
|
|
||||||
'client_email' => $email
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $results;}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -19,11 +19,19 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
|
|||||||
- **Pages / Articles UI** - ujednolicenie drzewek
|
- **Pages / Articles UI** - ujednolicenie drzewek
|
||||||
- UPDATE: `/admin/pages/list/` - nowe strzalki drzewa + `aria-expanded` + odswiezanie stanu branch/leaf
|
- UPDATE: `/admin/pages/list/` - nowe strzalki drzewa + `aria-expanded` + odswiezanie stanu branch/leaf
|
||||||
- UPDATE: `/admin/articles/edit/*` (zakladka wyswietlania) - nowe strzalki i checkboxy (iCheck) dla drzewka stron
|
- UPDATE: `/admin/articles/edit/*` (zakladka wyswietlania) - nowe strzalki i checkboxy (iCheck) dla drzewka stron
|
||||||
|
- **ShopClients** - migracja `/admin/shop_clients` na Domain + DI + nowe widoki
|
||||||
|
- NOWE: `Domain\Client\ClientRepository` (`listForAdmin`, `ordersForClient`, `totalsForClient`)
|
||||||
|
- NOWE: `admin\Controllers\ShopClientsController` (DI) z akcjami `list`, `details` + aliasy legacy `view_list`, `clients_details`
|
||||||
|
- UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ShopClients`
|
||||||
|
- UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_clients/list/`
|
||||||
|
- UPDATE: widoki `shop-clients/view-list` i `shop-clients/clients-details` przepiete na `components/table-list`
|
||||||
|
- CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopClients.php`, `autoload/admin/factory/class.ShopClients.php`
|
||||||
- TEST:
|
- TEST:
|
||||||
- NOWE: `tests/Unit/admin/Controllers/ShopProductControllerTest.php`
|
- NOWE: `tests/Unit/admin/Controllers/ShopProductControllerTest.php`
|
||||||
|
- NOWE: `tests/Unit/Domain/Client/ClientRepositoryTest.php`, `tests/Unit/admin/Controllers/ShopClientsControllerTest.php`
|
||||||
- UPDATE: `tests/Unit/Domain/Product/ProductRepositoryTest.php` (nowe przypadki dla mass_edit)
|
- UPDATE: `tests/Unit/Domain/Product/ProductRepositoryTest.php` (nowe przypadki dla mass_edit)
|
||||||
- UPDATE: `tests/bootstrap.php` (stub `S::normalize_decimal()`)
|
- UPDATE: `tests/bootstrap.php` (stub `S::normalize_decimal()`)
|
||||||
- Testy: **OK (351 tests, 1091 assertions)**
|
- Testy: **OK (361 tests, 1125 assertions)**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,28 @@ Przypisanie produktów do kategorii.
|
|||||||
|
|
||||||
**Aktualizacja 2026-02-15 (ver. 0.274):** akcje `/admin/shop_product/mass_edit/*` korzystają z `Domain\Product\ProductRepository` przez `admin\Controllers\ShopProductController`.
|
**Aktualizacja 2026-02-15 (ver. 0.274):** akcje `/admin/shop_product/mass_edit/*` korzystają z `Domain\Product\ProductRepository` przez `admin\Controllers\ShopProductController`.
|
||||||
|
|
||||||
|
## pp_shop_orders
|
||||||
|
Zamówienia sklepu (źródło danych dla list i szczegółów klientów w panelu admin).
|
||||||
|
|
||||||
|
| Kolumna | Opis |
|
||||||
|
|---------|------|
|
||||||
|
| id | PK |
|
||||||
|
| client_id | FK do `pp_shop_clients` (NULL dla gościa) |
|
||||||
|
| client_name | Imię klienta z zamówienia |
|
||||||
|
| client_surname | Nazwisko klienta z zamówienia |
|
||||||
|
| client_email | E-mail klienta z zamówienia |
|
||||||
|
| client_phone | Telefon klienta |
|
||||||
|
| client_city | Miasto klienta |
|
||||||
|
| summary | Wartość zamówienia |
|
||||||
|
| date_order | Data złożenia zamówienia |
|
||||||
|
| payment_method | Nazwa metody płatności |
|
||||||
|
| transport | Nazwa transportu |
|
||||||
|
| message | Wiadomość klienta |
|
||||||
|
|
||||||
|
**Używane w:** `Domain\Client\ClientRepository::listForAdmin()`, `Domain\Client\ClientRepository::ordersForClient()`, `Domain\Client\ClientRepository::totalsForClient()`.
|
||||||
|
|
||||||
|
**Aktualizacja 2026-02-15 (ver. 0.274):** moduł `/admin/shop_clients/*` korzysta z `Domain\Client\ClientRepository` przez `admin\Controllers\ShopClientsController`.
|
||||||
|
|
||||||
## pp_banners
|
## pp_banners
|
||||||
Banery.
|
Banery.
|
||||||
|
|
||||||
|
|||||||
@@ -298,6 +298,13 @@ Pelna dokumentacja testow: `TESTING.md`
|
|||||||
|
|
||||||
## Dodatkowa aktualizacja 2026-02-15 (ver. 0.273)
|
## Dodatkowa aktualizacja 2026-02-15 (ver. 0.273)
|
||||||
- Dodano modul domenowy `Domain/Producer/ProducerRepository.php`.
|
- Dodano modul domenowy `Domain/Producer/ProducerRepository.php`.
|
||||||
|
|
||||||
|
## Dodatkowa aktualizacja 2026-02-15 (ver. 0.274)
|
||||||
|
- Dodano modul domenowy `Domain/Client/ClientRepository.php`.
|
||||||
|
- Dodano kontroler DI `admin/Controllers/ShopClientsController.php`.
|
||||||
|
- Modul `/admin/shop_clients/*` dziala na nowych widokach opartych o `components/table-list`.
|
||||||
|
- Usunieto legacy: `autoload/admin/controls/class.ShopClients.php`, `autoload/admin/factory/class.ShopClients.php`.
|
||||||
|
- Routing i menu admin przepiete na kanoniczny URL `/admin/shop_clients/list/`.
|
||||||
- Dodano kontroler DI `admin/Controllers/ShopProducerController.php`.
|
- Dodano kontroler DI `admin/Controllers/ShopProducerController.php`.
|
||||||
- Modul `/admin/shop_producer/*` dziala na nowych widokach (`producers-list`, `producer-edit`).
|
- Modul `/admin/shop_producer/*` dziala na nowych widokach (`producers-list`, `producer-edit`).
|
||||||
- Usunieto legacy: `autoload/admin/controls/class.ShopProducer.php`, `admin/templates/shop-producer/list.php`, `admin/templates/shop-producer/edit.php`.
|
- Usunieto legacy: `autoload/admin/controls/class.ShopProducer.php`, `admin/templates/shop-producer/list.php`, `admin/templates/shop-producer/edit.php`.
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ grep -r "Product::getQuantity" .
|
|||||||
| 23 | ShopProductSets | 0.272 | listForAdmin, find, save, delete, allSets, allProductsMap, multi-select Selectize, DI kontroler |
|
| 23 | ShopProductSets | 0.272 | listForAdmin, find, save, delete, allSets, allProductsMap, multi-select Selectize, DI kontroler |
|
||||||
| 24 | ShopProducer | 0.273 | listForAdmin, find, save, delete, allProducers, producerProducts, fasada shop\Producer, DI kontroler |
|
| 24 | ShopProducer | 0.273 | listForAdmin, find, save, delete, allProducers, producerProducts, fasada shop\Producer, DI kontroler |
|
||||||
| 25 | ShopProduct (mass_edit) | 0.274 | DI kontroler + routing dla `mass_edit`, `mass_edit_save`, `get_products_by_category`, cleanup legacy akcji |
|
| 25 | ShopProduct (mass_edit) | 0.274 | DI kontroler + routing dla `mass_edit`, `mass_edit_save`, `get_products_by_category`, cleanup legacy akcji |
|
||||||
|
| 26 | ShopClients | 0.274 | DI kontroler + routing dla `list/details`, nowe listy na `components/table-list`, cleanup legacy controls/factory |
|
||||||
|
|
||||||
### Product - szczegolowy status
|
### Product - szczegolowy status
|
||||||
- ✅ getQuantity (ver. 0.238)
|
- ✅ getQuantity (ver. 0.238)
|
||||||
@@ -174,11 +175,11 @@ grep -r "Product::getQuantity" .
|
|||||||
|
|
||||||
## Kolejność refaktoryzacji (priorytet)
|
## Kolejność refaktoryzacji (priorytet)
|
||||||
|
|
||||||
1-25: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport, ShopAttribute, ShopProductSets, ShopProducer, ShopProduct (mass_edit)
|
1-26: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport, ShopAttribute, ShopProductSets, ShopProducer, ShopProduct (mass_edit), ShopClients
|
||||||
|
|
||||||
Nastepne:
|
Nastepne:
|
||||||
26. **Order**
|
27. **Order**
|
||||||
27. **Category**
|
28. **Category**
|
||||||
|
|
||||||
## Form Edit System
|
## Form Edit System
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ Ostatnio zweryfikowano: 2026-02-15
|
|||||||
OK (351 tests, 1091 assertions)
|
OK (351 tests, 1091 assertions)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Aktualizacja po migracji ShopClients (2026-02-15, ver. 0.274) - testy punktowe:
|
||||||
|
```text
|
||||||
|
OK (10 tests, 34 assertions)
|
||||||
|
```
|
||||||
|
|
||||||
|
Nowe testy dodane 2026-02-15:
|
||||||
|
- `tests/Unit/Domain/Client/ClientRepositoryTest.php`
|
||||||
|
- `tests/Unit/admin/Controllers/ShopClientsControllerTest.php`
|
||||||
|
|
||||||
## Struktura testow
|
## Struktura testow
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
134
tests/Unit/Domain/Client/ClientRepositoryTest.php
Normal file
134
tests/Unit/Domain/Client/ClientRepositoryTest.php
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
namespace Tests\Unit\Domain\Client;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Domain\Client\ClientRepository;
|
||||||
|
|
||||||
|
class ClientRepositoryTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testListForAdminWhitelistsSortAndPagination(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$queries = [];
|
||||||
|
|
||||||
|
$mockDb->method('query')
|
||||||
|
->willReturnCallback(function ($sql, $params = []) use (&$queries) {
|
||||||
|
$queries[] = ['sql' => $sql, 'params' => $params];
|
||||||
|
|
||||||
|
if (strpos($sql, 'COUNT(0)') !== false) {
|
||||||
|
return new class {
|
||||||
|
public function fetchAll() { return [[1]]; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new class {
|
||||||
|
public function fetchAll() {
|
||||||
|
return [[
|
||||||
|
'client_id' => 5,
|
||||||
|
'client_name' => 'Jan',
|
||||||
|
'client_surname' => 'Kowalski',
|
||||||
|
'client_email' => 'jan@example.com',
|
||||||
|
'client_phone' => '123',
|
||||||
|
'client_city' => 'Warszawa',
|
||||||
|
'total_orders' => 3,
|
||||||
|
'total_spent' => 199.99,
|
||||||
|
'is_registered' => 1,
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
$repository = new ClientRepository($mockDb);
|
||||||
|
$result = $repository->listForAdmin(
|
||||||
|
[],
|
||||||
|
'client_name DESC; DROP TABLE pp_shop_orders; --',
|
||||||
|
'DESC; DELETE FROM pp_users; --',
|
||||||
|
1,
|
||||||
|
999
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertCount(2, $queries);
|
||||||
|
$dataSql = $queries[1]['sql'];
|
||||||
|
|
||||||
|
$this->assertMatchesRegularExpression('/ORDER BY\s+c\.client_surname\s+ASC,\s+c\.client_surname\s+ASC,\s+c\.client_name\s+ASC/i', $dataSql);
|
||||||
|
$this->assertStringNotContainsString('DROP TABLE', $dataSql);
|
||||||
|
$this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql);
|
||||||
|
|
||||||
|
$this->assertSame(1, $result['total']);
|
||||||
|
$this->assertCount(1, $result['items']);
|
||||||
|
$this->assertSame('Jan', $result['items'][0]['client_name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOrdersForClientReturnsEmptyOnMissingInput(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->never())->method('query');
|
||||||
|
|
||||||
|
$repository = new ClientRepository($mockDb);
|
||||||
|
|
||||||
|
$this->assertSame([], $repository->ordersForClient('', 'Kowalski', 'jan@example.com'));
|
||||||
|
$this->assertSame([], $repository->ordersForClient('Jan', '', 'jan@example.com'));
|
||||||
|
$this->assertSame([], $repository->ordersForClient('Jan', 'Kowalski', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testOrdersForClientNormalizesRows(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('query')
|
||||||
|
->willReturn(new class {
|
||||||
|
public function fetchAll() {
|
||||||
|
return [[
|
||||||
|
'id' => '10',
|
||||||
|
'date_order' => '2026-02-15 10:00:00',
|
||||||
|
'summary' => '149.50',
|
||||||
|
'payment_method' => 'Przelew',
|
||||||
|
'transport' => 'Kurier',
|
||||||
|
'message' => null,
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$repository = new ClientRepository($mockDb);
|
||||||
|
$rows = $repository->ordersForClient('Jan', 'Kowalski', 'jan@example.com');
|
||||||
|
|
||||||
|
$this->assertCount(1, $rows);
|
||||||
|
$this->assertSame(10, $rows[0]['id']);
|
||||||
|
$this->assertSame(149.50, $rows[0]['summary']);
|
||||||
|
$this->assertSame('Przelew', $rows[0]['payment_method']);
|
||||||
|
$this->assertSame('', $rows[0]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTotalsForClientReturnsZeroForMissingInput(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->never())->method('query');
|
||||||
|
|
||||||
|
$repository = new ClientRepository($mockDb);
|
||||||
|
$totals = $repository->totalsForClient('', 'Kowalski', 'jan@example.com');
|
||||||
|
|
||||||
|
$this->assertSame(0, $totals['total_orders']);
|
||||||
|
$this->assertSame(0.0, $totals['total_spent']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTotalsForClientReturnsAggregatedValues(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('query')
|
||||||
|
->willReturn(new class {
|
||||||
|
public function fetchAll() {
|
||||||
|
return [[
|
||||||
|
'total_orders' => '4',
|
||||||
|
'total_spent' => '456.78',
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$repository = new ClientRepository($mockDb);
|
||||||
|
$totals = $repository->totalsForClient('Jan', 'Kowalski', 'jan@example.com');
|
||||||
|
|
||||||
|
$this->assertSame(4, $totals['total_orders']);
|
||||||
|
$this->assertSame(456.78, $totals['total_spent']);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
tests/Unit/admin/Controllers/ShopClientsControllerTest.php
Normal file
56
tests/Unit/admin/Controllers/ShopClientsControllerTest.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
namespace Tests\Unit\admin\Controllers;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use admin\Controllers\ShopClientsController;
|
||||||
|
use Domain\Client\ClientRepository;
|
||||||
|
|
||||||
|
class ShopClientsControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private $repository;
|
||||||
|
private $controller;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->repository = $this->createMock(ClientRepository::class);
|
||||||
|
$this->controller = new ShopClientsController($this->repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorAcceptsRepository(): void
|
||||||
|
{
|
||||||
|
$controller = new ShopClientsController($this->repository);
|
||||||
|
$this->assertInstanceOf(ShopClientsController::class, $controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasMainActionMethods(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'list'));
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'details'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHasLegacyAliasMethods(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'view_list'));
|
||||||
|
$this->assertTrue(method_exists($this->controller, 'clients_details'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testActionMethodReturnTypes(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass($this->controller);
|
||||||
|
|
||||||
|
$this->assertEquals('string', (string)$reflection->getMethod('list')->getReturnType());
|
||||||
|
$this->assertEquals('string', (string)$reflection->getMethod('view_list')->getReturnType());
|
||||||
|
$this->assertEquals('string', (string)$reflection->getMethod('details')->getReturnType());
|
||||||
|
$this->assertEquals('string', (string)$reflection->getMethod('clients_details')->getReturnType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorRequiresClientRepository(): void
|
||||||
|
{
|
||||||
|
$reflection = new \ReflectionClass(ShopClientsController::class);
|
||||||
|
$constructor = $reflection->getConstructor();
|
||||||
|
$params = $constructor->getParameters();
|
||||||
|
|
||||||
|
$this->assertCount(1, $params);
|
||||||
|
$this->assertEquals('Domain\\Client\\ClientRepository', $params[0]->getType()->getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
updates/0.20/ver_0.274.zip
Normal file
BIN
updates/0.20/ver_0.274.zip
Normal file
Binary file not shown.
2
updates/0.20/ver_0.274_files.txt
Normal file
2
updates/0.20/ver_0.274_files.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
F: ../autoload/admin/controls/class.ShopClients.php
|
||||||
|
F: ../autoload/admin/factory/class.ShopClients.php
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
|
<b>ver. 0.274 - 15.02.2026</b><br />
|
||||||
|
- NEW - migracja modulu `ShopClients` do architektury Domain + DI (`Domain\Client\ClientRepository`, `admin\Controllers\ShopClientsController`)
|
||||||
|
- UPDATE - modul `/admin/shop_clients/*` przepiety na `components/table-list` (lista klientow i szczegoly zamowien)
|
||||||
|
- UPDATE - routing i menu admin przepiete na kanoniczny URL `/admin/shop_clients/list/`
|
||||||
|
- CLEANUP - usuniete legacy klasy/pliki: `autoload/admin/controls/class.ShopClients.php`, `autoload/admin/factory/class.ShopClients.php`
|
||||||
|
- UPDATE - testy: `OK (361 tests, 1125 assertions)`
|
||||||
|
- UPDATE - pliki aktualizacji: `updates/0.20/ver_0.274.zip`, `ver_0.274_files.txt`
|
||||||
|
<hr>
|
||||||
<b>ver. 0.273 - 15.02.2026</b><br />
|
<b>ver. 0.273 - 15.02.2026</b><br />
|
||||||
- NEW - migracja `/admin/shop_product/mass_edit/*` do `Domain\Product\ProductRepository` + `admin\Controllers\ShopProductController` (DI + routing)
|
- NEW - migracja `/admin/shop_product/mass_edit/*` do `Domain\Product\ProductRepository` + `admin\Controllers\ShopProductController` (DI + routing)
|
||||||
- UPDATE - nowy widok/skrypt masowej edycji (`mass-edit`, `mass-edit-custom-script`) z iCheck i ujednoliconymi strzalkami drzewa
|
- UPDATE - nowy widok/skrypt masowej edycji (`mass-edit`, `mass-edit-custom-script`) z iCheck i ujednoliconymi strzalkami drzewa
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?
|
<?
|
||||||
$current_ver = 273;
|
$current_ver = 274;
|
||||||
|
|
||||||
for ($i = 1; $i <= $current_ver; $i++)
|
for ($i = 1; $i <= $current_ver; $i++)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user