diff --git a/admin/templates/components/table-list.php b/admin/templates/components/table-list.php index c6a91d4..831c9f4 100644 --- a/admin/templates/components/table-list.php +++ b/admin/templates/components/table-list.php @@ -270,6 +270,19 @@ $isCompactColumn = function(array $column): bool { diff --git a/autoload/Domain/Order/OrderAdminService.php b/autoload/Domain/Order/OrderAdminService.php new file mode 100644 index 0000000..8e11a50 --- /dev/null +++ b/autoload/Domain/Order/OrderAdminService.php @@ -0,0 +1,207 @@ +orders = $orders; + } + + public function details(int $orderId): array + { + return $this->orders->findForAdmin($orderId); + } + + public function statuses(): array + { + return $this->orders->orderStatuses(); + } + + /** + * @return array{items: array>, total: int} + */ + public function listForAdmin( + array $filters, + string $sortColumn = 'date_order', + string $sortDir = 'DESC', + int $page = 1, + int $perPage = 15 + ): array { + return $this->orders->listForAdmin($filters, $sortColumn, $sortDir, $page, $perPage); + } + + public function nextOrderId(int $orderId): ?int + { + return $this->orders->nextOrderId($orderId); + } + + public function prevOrderId(int $orderId): ?int + { + return $this->orders->prevOrderId($orderId); + } + + public function saveNotes(int $orderId, string $notes): bool + { + return $this->orders->saveNotes($orderId, $notes); + } + + public function saveOrderByAdmin(array $input): bool + { + $saved = $this->orders->saveOrderByAdmin( + (int)($input['order_id'] ?? 0), + (string)($input['client_name'] ?? ''), + (string)($input['client_surname'] ?? ''), + (string)($input['client_street'] ?? ''), + (string)($input['client_postal_code'] ?? ''), + (string)($input['client_city'] ?? ''), + (string)($input['client_email'] ?? ''), + (string)($input['firm_name'] ?? ''), + (string)($input['firm_street'] ?? ''), + (string)($input['firm_postal_code'] ?? ''), + (string)($input['firm_city'] ?? ''), + (string)($input['firm_nip'] ?? ''), + (int)($input['transport_id'] ?? 0), + (string)($input['inpost_paczkomat'] ?? ''), + (int)($input['payment_method_id'] ?? 0) + ); + + if ($saved && isset($GLOBALS['user']['id'])) { + \Log::save_log('Zamówienie zmienione przez administratora | ID: ' . (int)($input['order_id'] ?? 0), (int)$GLOBALS['user']['id']); + } + + return $saved; + } + + public function changeStatus(int $orderId, int $status, bool $sendEmail): array + { + $order = new \shop\Order($orderId); + $response = $order->update_status($status, $sendEmail ? 1 : 0); + + return is_array($response) ? $response : ['result' => false]; + } + + public function resendConfirmationEmail(int $orderId): bool + { + $order = new \shop\Order($orderId); + + return (bool)$order->order_resend_confirmation_email(); + } + + public function setOrderAsUnpaid(int $orderId): bool + { + $order = new \shop\Order($orderId); + + return (bool)$order->set_as_unpaid(); + } + + public function setOrderAsPaid(int $orderId, bool $sendMail): bool + { + $order = new \shop\Order($orderId); + if (!$order->set_as_paid()) { + return false; + } + + $order->update_status(4, $sendMail ? 1 : 0); + + return true; + } + + public function sendOrderToApilo(int $orderId): bool + { + global $mdb; + + if ($orderId <= 0) { + return false; + } + + $order = $this->orders->findForAdmin($orderId); + if (empty($order) || empty($order['apilo_order_id'])) { + return false; + } + + $integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb ); + $accessToken = $integrationsRepository -> apiloGetAccessToken(); + if (!$accessToken) { + return false; + } + + $newStatus = 8; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, 'https://projectpro.apilo.com/rest/api/orders/' . $order['apilo_order_id'] . '/status/'); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'id' => (int)$order['apilo_order_id'], + 'status' => (int)\front\factory\ShopStatuses::get_apilo_status_id($newStatus), + ])); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . $accessToken, + 'Accept: application/json', + 'Content-Type: application/json', + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $apiloResultRaw = curl_exec($ch); + $apiloResult = json_decode((string)$apiloResultRaw, true); + + if (!is_array($apiloResult) || (int)($apiloResult['updates'] ?? 0) !== 1) { + curl_close($ch); + return false; + } + + $query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_orders' AND COLUMN_NAME != 'id'"; + $columns = $mdb->query($query)->fetchAll(\PDO::FETCH_COLUMN); + $columnsList = implode(', ', $columns); + $mdb->query('INSERT INTO pp_shop_orders (' . $columnsList . ') SELECT ' . $columnsList . ' FROM pp_shop_orders pso WHERE pso.id = ' . $orderId); + $newOrderId = (int)$mdb->id(); + + $query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_order_products' AND COLUMN_NAME != 'id' AND COLUMN_NAME != 'order_id'"; + $columns = $mdb->query($query)->fetchAll(\PDO::FETCH_COLUMN); + $columnsList = implode(', ', $columns); + $mdb->query('INSERT INTO pp_shop_order_products (order_id, ' . $columnsList . ') SELECT ' . $newOrderId . ', ' . $columnsList . ' FROM pp_shop_order_products psop WHERE psop.order_id = ' . $orderId); + + $query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_order_statuses' AND COLUMN_NAME != 'id' AND COLUMN_NAME != 'order_id'"; + $columns = $mdb->query($query)->fetchAll(\PDO::FETCH_COLUMN); + $columnsList = implode(', ', $columns); + $mdb->query('INSERT INTO pp_shop_order_statuses (order_id, ' . $columnsList . ') SELECT ' . $newOrderId . ', ' . $columnsList . ' FROM pp_shop_order_statuses psos WHERE psos.order_id = ' . $orderId); + + $mdb->delete('pp_shop_orders', ['id' => $orderId]); + $mdb->delete('pp_shop_order_products', ['order_id' => $orderId]); + $mdb->delete('pp_shop_order_statuses', ['order_id' => $orderId]); + + $mdb->update('pp_shop_orders', ['apilo_order_id' => null], ['id' => $newOrderId]); + + curl_close($ch); + + return true; + } + + public function toggleTrustmateSend(int $orderId): array + { + $newValue = $this->orders->toggleTrustmateSend($orderId); + if ($newValue === null) { + return [ + 'result' => false, + ]; + } + + return [ + 'result' => true, + 'trustmate_send' => $newValue, + ]; + } + + public function deleteOrder(int $orderId): bool + { + $deleted = $this->orders->deleteOrder($orderId); + if ($deleted && isset($GLOBALS['user']['id'])) { + \Log::save_log('Usunięcie zamówienia | ID: ' . $orderId, (int)$GLOBALS['user']['id']); + } + + return $deleted; + } +} \ No newline at end of file diff --git a/autoload/Domain/Order/OrderRepository.php b/autoload/Domain/Order/OrderRepository.php new file mode 100644 index 0000000..fa7da85 --- /dev/null +++ b/autoload/Domain/Order/OrderRepository.php @@ -0,0 +1,472 @@ +db = $db; + } + + /** + * @return array{items: array>, total: int} + */ + public function listForAdmin( + array $filters, + string $sortColumn = 'date_order', + string $sortDir = 'DESC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'q1.id', + 'number' => 'q1.number', + 'date_order' => 'q1.date_order', + 'status' => 'q1.status', + 'summary' => 'q1.summary', + 'client' => 'q1.client', + 'order_email' => 'q1.order_email', + 'client_phone' => 'q1.client_phone', + 'transport' => 'q1.transport', + 'payment_method' => 'q1.payment_method', + 'total_orders' => 'shop_order.total_orders', + 'paid' => 'q1.paid', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'q1.date_order'; + $sortDir = strtoupper(trim($sortDir)) === 'ASC' ? 'ASC' : 'DESC'; + $page = max(1, $page); + $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); + $offset = ($page - 1) * $perPage; + + $where = []; + $params = []; + + $number = $this->normalizeTextFilter($filters['number'] ?? ''); + if ($number !== '') { + $where[] = 'q1.number LIKE :number'; + $params[':number'] = '%' . $number . '%'; + } + + $dateFrom = $this->normalizeDateFilter($filters['date_from'] ?? ''); + if ($dateFrom !== null) { + $where[] = 'q1.date_order >= :date_from'; + $params[':date_from'] = $dateFrom . ' 00:00:00'; + } + + $dateTo = $this->normalizeDateFilter($filters['date_to'] ?? ''); + if ($dateTo !== null) { + $where[] = 'q1.date_order <= :date_to'; + $params[':date_to'] = $dateTo . ' 23:59:59'; + } + + $status = trim((string)($filters['status'] ?? '')); + if ($status !== '' && is_numeric($status)) { + $where[] = 'q1.status = :status'; + $params[':status'] = (int)$status; + } + + $client = $this->normalizeTextFilter($filters['client'] ?? ''); + if ($client !== '') { + $where[] = 'q1.client LIKE :client'; + $params[':client'] = '%' . $client . '%'; + } + + $address = $this->normalizeTextFilter($filters['address'] ?? ''); + if ($address !== '') { + $where[] = 'q1.address LIKE :address'; + $params[':address'] = '%' . $address . '%'; + } + + $email = $this->normalizeTextFilter($filters['order_email'] ?? ''); + if ($email !== '') { + $where[] = 'q1.order_email LIKE :order_email'; + $params[':order_email'] = '%' . $email . '%'; + } + + $phone = $this->normalizeTextFilter($filters['client_phone'] ?? ''); + if ($phone !== '') { + $where[] = 'q1.client_phone LIKE :client_phone'; + $params[':client_phone'] = '%' . $phone . '%'; + } + + $transport = $this->normalizeTextFilter($filters['transport'] ?? ''); + if ($transport !== '') { + $where[] = 'q1.transport LIKE :transport'; + $params[':transport'] = '%' . $transport . '%'; + } + + $payment = $this->normalizeTextFilter($filters['payment_method'] ?? ''); + if ($payment !== '') { + $where[] = 'q1.payment_method LIKE :payment_method'; + $params[':payment_method'] = '%' . $payment . '%'; + } + + $whereSql = ''; + if (!empty($where)) { + $whereSql = ' WHERE ' . implode(' AND ', $where); + } + + $baseSql = " + FROM ( + SELECT + id, + number, + date_order, + CONCAT(client_name, ' ', client_surname) AS client, + client_email AS order_email, + CONCAT(client_street, ', ', client_postal_code, ' ', client_city) AS address, + status, + client_phone, + transport, + payment_method, + summary, + paid + FROM pp_shop_orders AS pso + ) AS q1 + LEFT JOIN ( + SELECT + client_email, + COUNT(*) AS total_orders + 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_email + ) AS shop_order ON q1.order_email = shop_order.client_email + "; + + $sqlCount = 'SELECT COUNT(0) ' . $baseSql . $whereSql; + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = 0; + if (is_array($countRows) && isset($countRows[0]) && is_array($countRows[0])) { + $firstRow = $countRows[0]; + $firstValue = reset($firstRow); + $total = $firstValue !== false ? (int)$firstValue : 0; + } + + $sql = ' + SELECT + q1.*, + COALESCE(shop_order.total_orders, 0) AS total_orders + ' + . $baseSql + . $whereSql + . ' ORDER BY ' . $sortSql . ' ' . $sortDir . ', q1.id DESC' + . ' LIMIT ' . $perPage . ' OFFSET ' . $offset; + + $stmt = $this->db->query($sql, $params); + if (!$stmt) { + return [ + 'items' => [], + 'total' => $total, + ]; + } + + $items = $stmt ? $stmt->fetchAll() : []; + if (!is_array($items)) { + $items = []; + } + + foreach ($items as &$item) { + $item['id'] = (int)($item['id'] ?? 0); + $item['status'] = (int)($item['status'] ?? 0); + $item['paid'] = (int)($item['paid'] ?? 0); + $item['summary'] = (float)($item['summary'] ?? 0); + $item['total_orders'] = (int)($item['total_orders'] ?? 0); + $item['number'] = (string)($item['number'] ?? ''); + $item['date_order'] = (string)($item['date_order'] ?? ''); + $item['client'] = trim((string)($item['client'] ?? '')); + $item['order_email'] = (string)($item['order_email'] ?? ''); + $item['address'] = trim((string)($item['address'] ?? '')); + $item['client_phone'] = (string)($item['client_phone'] ?? ''); + $item['transport'] = (string)($item['transport'] ?? ''); + $item['payment_method'] = (string)($item['payment_method'] ?? ''); + } + unset($item); + + return [ + 'items' => $items, + 'total' => $total, + ]; + } + + public function findForAdmin(int $orderId): array + { + if ($orderId <= 0) { + return []; + } + + $order = $this->db->get('pp_shop_orders', '*', ['id' => $orderId]); + if (!is_array($order)) { + return []; + } + + $order['id'] = (int)($order['id'] ?? 0); + $order['status'] = (int)($order['status'] ?? 0); + $order['paid'] = (int)($order['paid'] ?? 0); + $order['summary'] = (float)($order['summary'] ?? 0); + $order['transport_cost'] = (float)($order['transport_cost'] ?? 0); + $order['products'] = $this->orderProducts($orderId); + $order['statuses'] = $this->orderStatusHistory($orderId); + + return $order; + } + + public function orderProducts(int $orderId): array + { + if ($orderId <= 0) { + return []; + } + + $rows = $this->db->select('pp_shop_order_products', '*', [ + 'order_id' => $orderId, + ]); + + return is_array($rows) ? $rows : []; + } + + public function orderStatusHistory(int $orderId): array + { + if ($orderId <= 0) { + return []; + } + + $rows = $this->db->select('pp_shop_order_statuses', '*', [ + 'order_id' => $orderId, + 'ORDER' => ['id' => 'DESC'], + ]); + + return is_array($rows) ? $rows : []; + } + + public function orderStatuses(): array + { + $rows = $this->db->select('pp_shop_statuses', ['id', 'status'], [ + 'ORDER' => ['o' => 'ASC'], + ]); + + if (!is_array($rows)) { + return []; + } + + $result = []; + foreach ($rows as $row) { + $id = (int)($row['id'] ?? 0); + if ($id < 0) { + continue; + } + + $result[$id] = (string)($row['status'] ?? ''); + } + + return $result; + } + + public function nextOrderId(int $orderId): ?int + { + if ($orderId <= 0) { + return null; + } + + $next = $this->db->get('pp_shop_orders', 'id', [ + 'id[>]' => $orderId, + 'ORDER' => ['id' => 'ASC'], + 'LIMIT' => 1, + ]); + + if (!$next) { + return null; + } + + return (int)$next; + } + + public function prevOrderId(int $orderId): ?int + { + if ($orderId <= 0) { + return null; + } + + $prev = $this->db->get('pp_shop_orders', 'id', [ + 'id[<]' => $orderId, + 'ORDER' => ['id' => 'DESC'], + 'LIMIT' => 1, + ]); + + if (!$prev) { + return null; + } + + return (int)$prev; + } + + public function saveNotes(int $orderId, string $notes): bool + { + if ($orderId <= 0) { + return false; + } + + $this->db->update('pp_shop_orders', ['notes' => $notes], ['id' => $orderId]); + + return true; + } + + public function saveOrderByAdmin( + int $orderId, + string $clientName, + string $clientSurname, + string $clientStreet, + string $clientPostalCode, + string $clientCity, + string $clientEmail, + string $firmName, + string $firmStreet, + string $firmPostalCode, + string $firmCity, + string $firmNip, + int $transportId, + string $inpostPaczkomat, + int $paymentMethodId + ): bool { + if ($orderId <= 0) { + return false; + } + + $transportName = $this->db->get('pp_shop_transports', 'name_visible', ['id' => $transportId]); + $transportCost = $this->db->get('pp_shop_transports', 'cost', ['id' => $transportId]); + $transportDescription = $this->db->get('pp_shop_transports', 'description', ['id' => $transportId]); + $paymentMethodName = $this->db->get('pp_shop_payment_methods', 'name', ['id' => $paymentMethodId]); + + $this->db->update('pp_shop_orders', [ + 'client_name' => $clientName, + 'client_surname' => $clientSurname, + 'client_street' => $clientStreet, + 'client_postal_code' => $clientPostalCode, + 'client_city' => $clientCity, + 'client_email' => $clientEmail, + 'firm_name' => $this->nullableString($firmName), + 'firm_street' => $this->nullableString($firmStreet), + 'firm_postal_code' => $this->nullableString($firmPostalCode), + 'firm_city' => $this->nullableString($firmCity), + 'firm_nip' => $this->nullableString($firmNip), + 'transport_id' => $transportId, + 'transport' => $transportName ?: null, + 'transport_cost' => $transportCost !== null ? $transportCost : 0, + 'transport_description' => $transportDescription ?: null, + 'inpost_paczkomat' => $inpostPaczkomat, + 'payment_method_id' => $paymentMethodId, + 'payment_method' => $paymentMethodName ?: null, + ], [ + 'id' => $orderId, + ]); + + $this->db->update('pp_shop_orders', [ + 'summary' => $this->calculateOrderSummaryByAdmin($orderId), + ], [ + 'id' => $orderId, + ]); + + return true; + } + + public function calculateOrderSummaryByAdmin(int $orderId): float + { + if ($orderId <= 0) { + return 0.0; + } + + $rows = $this->db->select('pp_shop_order_products', [ + 'price_brutto', + 'price_brutto_promo', + 'quantity', + ], [ + 'order_id' => $orderId, + ]); + + $summary = 0.0; + if (is_array($rows)) { + foreach ($rows as $row) { + $quantity = (float)($row['quantity'] ?? 0); + $pricePromo = (float)($row['price_brutto_promo'] ?? 0); + $price = (float)($row['price_brutto'] ?? 0); + + if ($pricePromo > 0) { + $summary += $pricePromo * $quantity; + } else { + $summary += $price * $quantity; + } + } + } + + $transportCost = (float)$this->db->get('pp_shop_orders', 'transport_cost', ['id' => $orderId]); + + return (float)$summary + $transportCost; + } + + public function toggleTrustmateSend(int $orderId): ?int + { + if ($orderId <= 0) { + return null; + } + + $order = $this->db->get('pp_shop_orders', ['trustmate_send'], ['id' => $orderId]); + if (!is_array($order)) { + return null; + } + + $newValue = ((int)($order['trustmate_send'] ?? 0) === 1) ? 0 : 1; + $this->db->update('pp_shop_orders', ['trustmate_send' => $newValue], ['id' => $orderId]); + + return $newValue; + } + + public function deleteOrder(int $orderId): bool + { + if ($orderId <= 0) { + return false; + } + + $this->db->delete('pp_shop_orders', ['id' => $orderId]); + + return true; + } + + private function nullableString(string $value): ?string + { + $value = trim($value); + + return $value === '' ? null : $value; + } + + private function normalizeTextFilter($value): string + { + $value = trim((string)$value); + if ($value === '') { + return ''; + } + + if (strlen($value) > 255) { + return substr($value, 0, 255); + } + + return $value; + } + + private function normalizeDateFilter($value): ?string + { + $value = trim((string)$value); + if ($value === '') { + return null; + } + + if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { + return null; + } + + return $value; + } +} \ No newline at end of file diff --git a/autoload/admin/Controllers/SettingsController.php b/autoload/admin/Controllers/SettingsController.php index 35b65c0..3fb7d36 100644 --- a/autoload/admin/Controllers/SettingsController.php +++ b/autoload/admin/Controllers/SettingsController.php @@ -68,6 +68,159 @@ class SettingsController exit; } + /** + * Globalna wyszukiwarka admin (produkty + zamowienia) - AJAX. + */ + public function globalSearchAjax(): void + { + global $mdb; + + $phrase = trim((string)\S::get('q')); + if ($phrase === '' || mb_strlen($phrase) < 2) { + echo json_encode([ + 'status' => 'ok', + 'items' => [], + ]); + exit; + } + + $phrase = mb_substr($phrase, 0, 120); + $phraseNormalized = preg_replace('/\s+/', ' ', $phrase); + $phraseNormalized = trim((string)$phraseNormalized); + $like = '%' . $phrase . '%'; + $likeNormalized = '%' . $phraseNormalized . '%'; + + $items = []; + $defaultLang = (string)\front\factory\Languages::default_language(); + + try { + $productStmt = $mdb->query( + 'SELECT ' + . 'p.id, p.ean, p.sku, p.parent_id, psl.name ' + . 'FROM pp_shop_products AS p ' + . 'LEFT JOIN pp_shop_products_langs AS psl ON psl.product_id = p.id AND psl.lang_id = :lang_id ' + . 'WHERE ' + . '(p.ean LIKE :q1 OR p.sku LIKE :q2 OR psl.name LIKE :q3) ' + . 'AND p.archive != 1 ' + . 'ORDER BY p.id DESC ' + . 'LIMIT 15', + [ + ':lang_id' => $defaultLang, + ':q1' => $like, + ':q2' => $like, + ':q3' => $like, + ] + ); + } catch (\Throwable $e) { + $productStmt = false; + } + + $productRows = $productStmt ? $productStmt->fetchAll() : []; + if (is_array($productRows)) { + foreach ($productRows as $row) { + $productId = (int)($row['id'] ?? 0); + if ($productId <= 0) { + continue; + } + + $name = trim((string)($row['name'] ?? '')); + if ($name === '') { + $name = 'Produkt #' . $productId; + } + + $meta = []; + $sku = trim((string)($row['sku'] ?? '')); + $ean = trim((string)($row['ean'] ?? '')); + if ($sku !== '') { + $meta[] = 'SKU: ' . $sku; + } + if ($ean !== '') { + $meta[] = 'EAN: ' . $ean; + } + + $items[] = [ + 'type' => 'product', + 'title' => $name, + 'subtitle' => implode(' | ', $meta), + 'url' => '/admin/shop_product/product_edit/id=' . $productId, + ]; + } + } + + try { + $orderStmt = $mdb->query( + 'SELECT ' + . 'id, number, client_name, client_surname, client_email, client_phone ' + . 'FROM pp_shop_orders ' + . 'WHERE ' + . '(' + . 'number LIKE :q1 ' + . 'OR client_email LIKE :q2 ' + . 'OR client_name LIKE :q3 ' + . 'OR client_surname LIKE :q4 ' + . 'OR client_phone LIKE :q5 ' + . "OR CONCAT_WS(' ', TRIM(client_name), TRIM(client_surname)) LIKE :q6 " + . "OR CONCAT_WS(' ', TRIM(client_surname), TRIM(client_name)) LIKE :q7 " + . ') ' + . 'ORDER BY id DESC ' + . 'LIMIT 15', + [ + ':q1' => $like, + ':q2' => $like, + ':q3' => $like, + ':q4' => $like, + ':q5' => $like, + ':q6' => $likeNormalized, + ':q7' => $likeNormalized, + ] + ); + } catch (\Throwable $e) { + $orderStmt = false; + } + + $orderRows = $orderStmt ? $orderStmt->fetchAll() : []; + if (is_array($orderRows)) { + foreach ($orderRows as $row) { + $orderId = (int)($row['id'] ?? 0); + if ($orderId <= 0) { + continue; + } + + $orderNumber = trim((string)($row['number'] ?? '')); + $clientName = trim((string)($row['client_name'] ?? '')); + $clientSurname = trim((string)($row['client_surname'] ?? '')); + $clientEmail = trim((string)($row['client_email'] ?? '')); + $clientPhone = trim((string)($row['client_phone'] ?? '')); + + $title = $orderNumber !== '' ? 'Zamówienie ' . $orderNumber : 'Zamówienie #' . $orderId; + $subtitleParts = []; + $fullName = trim($clientName . ' ' . $clientSurname); + if ($fullName !== '') { + $subtitleParts[] = $fullName; + } + if ($clientEmail !== '') { + $subtitleParts[] = $clientEmail; + } + if ($clientPhone !== '') { + $subtitleParts[] = $clientPhone; + } + + $items[] = [ + 'type' => 'order', + 'title' => $title, + 'subtitle' => implode(' | ', $subtitleParts), + 'url' => '/admin/shop_order/order_details/order_id=' . $orderId, + ]; + } + } + + echo json_encode([ + 'status' => 'ok', + 'items' => array_slice($items, 0, 20), + ]); + exit; + } + /** * Zapis ustawien (AJAX). */ diff --git a/autoload/admin/Controllers/ShopOrderController.php b/autoload/admin/Controllers/ShopOrderController.php new file mode 100644 index 0000000..207f4a9 --- /dev/null +++ b/autoload/admin/Controllers/ShopOrderController.php @@ -0,0 +1,323 @@ +service = $service; + } + + public function list(): string + { + return $this->view_list(); + } + + public function view_list(): string + { + $sortableColumns = [ + 'number', + 'date_order', + 'status', + 'summary', + 'client', + 'order_email', + 'client_phone', + 'transport', + 'payment_method', + 'total_orders', + 'paid', + ]; + + $statusOptions = ['' => '- status -']; + foreach ($this->service->statuses() as $statusId => $statusName) { + $statusOptions[(string)$statusId] = (string)$statusName; + } + + $filterDefinitions = [ + ['key' => 'number', 'label' => 'Nr zamówienia', 'type' => 'text'], + ['key' => 'date_from', 'label' => 'Data od', 'type' => 'date'], + ['key' => 'date_to', 'label' => 'Data do', 'type' => 'date'], + ['key' => 'status', 'label' => 'Status', 'type' => 'select', 'options' => $statusOptions], + ['key' => 'client', 'label' => 'Klient', 'type' => 'text'], + ['key' => 'address', 'label' => 'Adres', 'type' => 'text'], + ['key' => 'order_email', 'label' => 'Email', 'type' => 'text'], + ['key' => 'client_phone', 'label' => 'Telefon', 'type' => 'text'], + ['key' => 'transport', 'label' => 'Dostawa', 'type' => 'text'], + ['key' => 'payment_method', 'label' => 'Płatność', 'type' => 'text'], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'date_order' + ); + + $result = $this->service->listForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $listRequest['sortDir'], + $listRequest['page'], + $listRequest['perPage'] + ); + + $statusesMap = $this->service->statuses(); + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + + foreach ($result['items'] as $item) { + $orderId = (int)($item['id'] ?? 0); + $orderNumber = (string)($item['number'] ?? ''); + $statusId = (int)($item['status'] ?? 0); + $statusLabel = (string)($statusesMap[$statusId] ?? ('Status #' . $statusId)); + + $rows[] = [ + 'lp' => $lp++ . '.', + 'date_order' => $this->formatDateTime((string)($item['date_order'] ?? '')), + 'number' => '' . htmlspecialchars($orderNumber, ENT_QUOTES, 'UTF-8') . '', + 'paid' => ((int)($item['paid'] ?? 0) === 1) + ? '' + : '', + 'status' => htmlspecialchars($statusLabel, ENT_QUOTES, 'UTF-8'), + 'summary' => number_format((float)($item['summary'] ?? 0), 2, '.', ' ') . ' zł', + 'client' => htmlspecialchars((string)($item['client'] ?? ''), ENT_QUOTES, 'UTF-8') . ' | zamówienia: ' . (int)($item['total_orders'] ?? 0) . '', + 'address' => (string)($item['address'] ?? ''), + 'order_email' => (string)($item['order_email'] ?? ''), + 'client_phone' => (string)($item['client_phone'] ?? ''), + 'transport' => (string)($item['transport'] ?? ''), + 'payment_method' => (string)($item['payment_method'] ?? ''), + '_actions' => [ + [ + 'label' => 'Szczegóły', + 'url' => '/admin/shop_order/order_details/order_id=' . $orderId, + 'class' => 'btn btn-xs btn-primary', + ], + [ + 'label' => 'Usuń', + 'url' => '/admin/shop_order/order_delete/id=' . $orderId, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'Na pewno chcesz usunąć wybrane zamówienie?', + 'confirm_ok' => 'Usuń', + 'confirm_cancel' => 'Anuluj', + ], + ], + ]; + } + + $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' => 'date_order', 'sort_key' => 'date_order', 'label' => 'Data dodania', 'class' => 'text-center', 'sortable' => true], + ['key' => 'number', 'sort_key' => 'number', 'label' => 'Nr zamówienia', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'paid', 'sort_key' => 'paid', 'label' => '', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'status', 'sort_key' => 'status', 'label' => 'Status', 'sortable' => true, 'raw' => true], + ['key' => 'summary', 'sort_key' => 'summary', 'label' => 'Wartość', 'class' => 'text-right align-middle', 'sortable' => true], + ['key' => 'client', 'sort_key' => 'client', 'label' => 'Klient', 'sortable' => true, 'raw' => true], + ['key' => 'address', 'label' => 'Adres', 'sortable' => false], + ['key' => 'order_email', 'sort_key' => 'order_email', 'label' => 'Email', 'sortable' => true], + ['key' => 'client_phone', 'sort_key' => 'client_phone', 'label' => 'Telefon', 'sortable' => true], + ['key' => 'transport', 'sort_key' => 'transport', 'label' => 'Dostawa', 'sortable' => true], + ['key' => 'payment_method', 'sort_key' => 'payment_method', 'label' => 'Płatność', 'sortable' => true], + ], + $rows, + $listRequest['viewFilters'], + [ + 'column' => $listRequest['sortColumn'], + 'dir' => $listRequest['sortDir'], + ], + [ + 'page' => $listRequest['page'], + 'per_page' => $listRequest['perPage'], + 'total' => $total, + 'total_pages' => $totalPages, + ], + array_merge($listRequest['queryFilters'], [ + 'sort' => $listRequest['sortColumn'], + 'dir' => $listRequest['sortDir'], + 'per_page' => $listRequest['perPage'], + ]), + $listRequest['perPageOptions'], + $sortableColumns, + '/admin/shop_order/list/', + 'Brak danych w tabeli.' + ); + + return \Tpl::view('shop-order/orders-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function details(): string + { + return $this->order_details(); + } + + public function order_details(): string + { + $orderId = (int)\S::get('order_id'); + $order = $this->service->details($orderId); + + $coupon = null; + if (!empty($order) && !empty($order['coupon_id'])) { + $coupon = new \shop\Coupon((int)$order['coupon_id']); + } + + return \Tpl::view('shop-order/order-details', [ + 'order' => $order, + 'coupon' => $coupon, + 'order_statuses' => $this->service->statuses(), + 'next_order_id' => $this->service->nextOrderId($orderId), + 'prev_order_id' => $this->service->prevOrderId($orderId), + ]); + } + + public function edit(): string + { + return $this->order_edit(); + } + + public function order_edit(): string + { + $orderId = (int)\S::get('order_id'); + + return \Tpl::view('shop-order/order-edit', [ + 'order' => $this->service->details($orderId), + 'order_statuses' => $this->service->statuses(), + 'transport' => \shop\Transport::transport_list(), + 'payment_methods' => \shop\PaymentMethod::method_list(), + ]); + } + + public function save(): void + { + $this->order_save(); + } + + public function order_save(): void + { + $saved = $this->service->saveOrderByAdmin([ + 'order_id' => (int)\S::get('order_id'), + 'client_name' => (string)\S::get('client_name'), + 'client_surname' => (string)\S::get('client_surname'), + 'client_street' => (string)\S::get('client_street'), + 'client_postal_code' => (string)\S::get('client_postal_code'), + 'client_city' => (string)\S::get('client_city'), + 'client_email' => (string)\S::get('client_email'), + 'firm_name' => (string)\S::get('firm_name'), + 'firm_street' => (string)\S::get('firm_street'), + 'firm_postal_code' => (string)\S::get('firm_postal_code'), + 'firm_city' => (string)\S::get('firm_city'), + 'firm_nip' => (string)\S::get('firm_nip'), + 'transport_id' => (int)\S::get('transport_id'), + 'inpost_paczkomat' => (string)\S::get('inpost_paczkomat'), + 'payment_method_id' => (int)\S::get('payment_method_id'), + ]); + + if ($saved) { + \S::alert('Zamówienie zostało zapisane.'); + } + + header('Location: /admin/shop_order/order_details/order_id=' . (int)\S::get('order_id')); + exit; + } + + public function notes_save(): void + { + $this->service->saveNotes((int)\S::get('order_id'), (string)\S::get('notes')); + } + + public function order_status_change(): void + { + $response = $this->service->changeStatus( + (int)\S::get('order_id'), + (int)\S::get('status'), + (string)\S::get('email') === 'true' + ); + + echo json_encode($response); + exit; + } + + public function order_resend_confirmation_email(): void + { + $response = $this->service->resendConfirmationEmail((int)\S::get('order_id')); + + echo json_encode(['result' => $response]); + exit; + } + + public function set_order_as_unpaid(): void + { + $orderId = (int)\S::get('order_id'); + $this->service->setOrderAsUnpaid($orderId); + + header('Location: /admin/shop_order/order_details/order_id=' . $orderId); + exit; + } + + public function set_order_as_paid(): void + { + $orderId = (int)\S::get('order_id'); + $this->service->setOrderAsPaid($orderId, (int)\S::get('send_mail') === 1); + + header('Location: /admin/shop_order/order_details/order_id=' . $orderId); + exit; + } + + public function send_order_to_apilo(): void + { + $orderId = (int)\S::get('order_id'); + + if ($this->service->sendOrderToApilo($orderId)) { + \S::alert('Zamówienie zostanie wysłane ponownie do apilo.com'); + } else { + \S::alert('Wystąpił błąd podczas wysyłania zamówienia do apilo.com'); + } + + header('Location: /admin/shop_order/order_details/order_id=' . $orderId); + exit; + } + + public function toggle_trustmate_send(): void + { + echo json_encode($this->service->toggleTrustmateSend((int)\S::get('order_id'))); + exit; + } + + public function delete(): void + { + $this->order_delete(); + } + + public function order_delete(): void + { + if ($this->service->deleteOrder((int)\S::get('id'))) { + \S::alert('Zamówienie zostało usunięte'); + } + + header('Location: /admin/shop_order/list/'); + exit; + } + + private function formatDateTime(string $value): string + { + if ($value === '') { + return ''; + } + + $ts = strtotime($value); + if ($ts === false) { + return $value; + } + + return date('Y-m-d H:i', $ts); + } +} \ No newline at end of file diff --git a/autoload/admin/Controllers/ShopPaymentMethodController.php b/autoload/admin/Controllers/ShopPaymentMethodController.php index e72e07a..6ff4352 100644 --- a/autoload/admin/Controllers/ShopPaymentMethodController.php +++ b/autoload/admin/Controllers/ShopPaymentMethodController.php @@ -2,6 +2,7 @@ namespace admin\Controllers; use Domain\PaymentMethod\PaymentMethodRepository; +use Domain\Integrations\IntegrationsRepository; use admin\ViewModels\Common\PaginatedTableViewModel; use admin\ViewModels\Forms\FormAction; use admin\ViewModels\Forms\FormEditViewModel; @@ -240,7 +241,10 @@ class ShopPaymentMethodController private function getApiloPaymentTypes(): array { - $rawSetting = \admin\factory\Integrations::apilo_settings('payment-types-list'); + global $mdb; + + $integrationsRepository = new IntegrationsRepository( $mdb ); + $rawSetting = $integrationsRepository -> getSetting( 'apilo', 'payment-types-list' ); $raw = null; if (is_array($rawSetting)) { diff --git a/autoload/admin/Controllers/ShopStatusesController.php b/autoload/admin/Controllers/ShopStatusesController.php index 1845cfe..22ac7c3 100644 --- a/autoload/admin/Controllers/ShopStatusesController.php +++ b/autoload/admin/Controllers/ShopStatusesController.php @@ -2,6 +2,7 @@ namespace admin\Controllers; use Domain\ShopStatus\ShopStatusRepository; +use Domain\Integrations\IntegrationsRepository; use admin\ViewModels\Common\PaginatedTableViewModel; use admin\ViewModels\Forms\FormAction; use admin\ViewModels\Forms\FormEditViewModel; @@ -246,8 +247,12 @@ class ShopStatusesController private function getApiloStatusList(): array { + global $mdb; + + $integrationsRepository = new IntegrationsRepository( $mdb ); + $list = []; - $raw = @unserialize(\admin\factory\Integrations::apilo_settings('status-types-list')); + $raw = @unserialize( $integrationsRepository -> getSetting( 'apilo', 'status-types-list' ) ); if (is_array($raw)) { foreach ($raw as $apiloStatus) { if (isset($apiloStatus['id'], $apiloStatus['name'])) { diff --git a/autoload/admin/Controllers/ShopTransportController.php b/autoload/admin/Controllers/ShopTransportController.php index 65fc5ae..ebe160a 100644 --- a/autoload/admin/Controllers/ShopTransportController.php +++ b/autoload/admin/Controllers/ShopTransportController.php @@ -3,6 +3,7 @@ namespace admin\Controllers; use Domain\Transport\TransportRepository; use Domain\PaymentMethod\PaymentMethodRepository; +use Domain\Integrations\IntegrationsRepository; use admin\ViewModels\Common\PaginatedTableViewModel; use admin\ViewModels\Forms\FormAction; use admin\ViewModels\Forms\FormEditViewModel; @@ -314,7 +315,10 @@ class ShopTransportController private function getApiloCarrierAccounts(): array { - $rawSetting = \admin\factory\Integrations::apilo_settings('carrier-account-list'); + global $mdb; + + $integrationsRepository = new IntegrationsRepository( $mdb ); + $rawSetting = $integrationsRepository -> getSetting( 'apilo', 'carrier-account-list' ); $raw = null; if (is_array($rawSetting)) { diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 2d6c1ad..119f4c9 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -399,6 +399,15 @@ class Site new \Domain\Client\ClientRepository( $mdb ) ); }, + 'ShopOrder' => function() { + global $mdb; + + return new \admin\Controllers\ShopOrderController( + new \Domain\Order\OrderAdminService( + new \Domain\Order\OrderRepository( $mdb ) + ) + ); + }, ]; return self::$newControllers; diff --git a/autoload/admin/controls/class.Dashboard.php b/autoload/admin/controls/class.Dashboard.php index fbd00d1..b8f2043 100644 --- a/autoload/admin/controls/class.Dashboard.php +++ b/autoload/admin/controls/class.Dashboard.php @@ -4,9 +4,13 @@ class Dashboard { static public function main_view() { + global $mdb; + + $statusesRepository = new \Domain\ShopStatus\ShopStatusRepository( $mdb ); + return \Tpl::view( 'dashboard/main-view', [ 'last_orders' => \shop\Dashboard::last_orders(), - 'order_statuses' => \shop\Order::order_statuses(), + 'order_statuses' => $statusesRepository -> allStatuses(), 'sales' => \shop\Dashboard::last_24_months_sales(), 'best_sales_products' => \shop\Dashboard::best_sales_products(), 'most_view_products' => \shop\Dashboard::most_view_products(), diff --git a/autoload/admin/controls/class.ShopOrder.php b/autoload/admin/controls/class.ShopOrder.php deleted file mode 100644 index fb0e569..0000000 --- a/autoload/admin/controls/class.ShopOrder.php +++ /dev/null @@ -1,133 +0,0 @@ - order_resend_confirmation_email(); - - echo json_encode( [ 'result' => $response ] ); - exit; - } - - static public function notes_save() - { - \shop\Order::notes_save( \S::get( 'order_id' ), \S::get( 'notes' ) ); - } - - static public function order_save() - { - if ( \shop\Order::order_save_by_admin( - \S::get( 'order_id' ), \S::get( 'client_name' ), \S::get( 'client_surname' ), \S::get( 'client_street' ), \S::get( 'client_postal_code' ), \S::get( 'client_city' ), \S::get( 'client_email' ), \S::get( 'firm_name' ), \S::get( 'firm_street' ), \S::get( 'firm_postal_code' ), \S::get( 'firm_city' ), \S::get( 'firm_nip' ), \S::get( 'transport_id' ), \S::get( 'inpost_paczkomat' ), \S::get( 'payment_method_id' ) - ) ) - \S::alert( 'Zamówienie zostało zapisane.' ); - - header( 'Location: /admin/shop_order/order_details/order_id=' . \S::get( 'order_id' ) ); - exit; - } - - static public function order_edit() - { - return \Tpl::view( 'shop-order/order-edit', [ - 'order' => new \shop\Order( (int)\S::get( 'order_id' ) ), - 'order_statuses' => \shop\Order::order_statuses(), - 'transport' => \shop\Transport::transport_list(), - 'payment_methods' => \shop\PaymentMethod::method_list() - ] ); - } - - public static function order_details() - { - $order = new \shop\Order( (int)\S::get( 'order_id' ) ); - $coupon = $order -> coupon_id ? new \shop\Coupon( $order -> coupon_id ) : null; - - return \Tpl::view( 'shop-order/order-details', [ - 'order' => $order, - 'coupon' => $coupon, - 'order_statuses' => \shop\Order::order_statuses(), - 'next_order_id' => \admin\factory\ShopOrder::next_order_id( (int)\S::get( 'order_id' ) ), - 'prev_order_id' => \admin\factory\ShopOrder::prev_order_id( (int)\S::get( 'order_id' ) ), - ] ); - } - - public static function view_list() - { - return \Tpl::view( - 'shop-order/view-list' - ); - } - - public static function order_status_change() - { - global $mdb; - - $order = new \shop\Order( (int)\S::get( 'order_id' ) ); - $response = $order -> update_status( (int)\S::get( 'status' ), \S::get( 'email' ) == 'true' ? 1 : 0 ); - - echo json_encode( $response ); - exit; - } - - public static function order_delete() - { - global $user; - - if ( \shop\Order::order_delete( (int)\S::get( 'id' ) ) ) - { - \S::alert( 'Zamówienie zostało usunięte' ); - \Log::save_log( 'Usunięcie zamówienia | ID: ' . (int)\S::get( 'id' ), $user['id'] ); - } - - header( 'Location: /admin/shop_order/view_list/' ); - exit; - } - - // set_order_as_unpaid - public static function set_order_as_unpaid() - { - $order = new \shop\Order( (int)\S::get( 'order_id' ) ); - $order -> set_as_unpaid(); - header( 'Location: /admin/shop_order/order_details/order_id=' . (int)\S::get( 'order_id' ) ); - exit; - } - - // set_order_as_paid - public static function set_order_as_paid() - { - $order = new \shop\Order( (int)\S::get( 'order_id' ) ); - if ( $order -> set_as_paid() ) - { - $order -> update_status( 4, (int)\S::get( 'send_mail' ) ); - } - header( 'Location: /admin/shop_order/order_details/order_id=' . (int)\S::get( 'order_id' ) ); - exit; - } - - // toggle_trustmate_send - public static function toggle_trustmate_send() - { - global $mdb; - - $order_id = (int)\S::get( 'order_id' ); - $order = $mdb -> get( 'pp_shop_orders', [ 'trustmate_send' ], [ 'id' => $order_id ] ); - - $new_value = $order['trustmate_send'] ? 0 : 1; - $mdb -> update( 'pp_shop_orders', [ 'trustmate_send' => $new_value ], [ 'id' => $order_id ] ); - - echo json_encode( [ 'result' => true, 'trustmate_send' => $new_value ] ); - exit; - } -} diff --git a/autoload/admin/controls/class.ShopProduct.php b/autoload/admin/controls/class.ShopProduct.php index c7113d4..3227e3c 100644 --- a/autoload/admin/controls/class.ShopProduct.php +++ b/autoload/admin/controls/class.ShopProduct.php @@ -229,6 +229,10 @@ class ShopProduct // ajax_load_products static public function ajax_load_products() { + global $mdb; + + $integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb ); + $response = [ 'status' => 'error', 'msg' => 'Podczas ładowania produktów wystąpił błąd. Proszę spróbować ponownie.' ]; \S::set_session( 'products_list_current_page', \S::get( 'current_page' ) ); @@ -241,7 +245,7 @@ class ShopProduct 'html' => \Tpl::view( 'shop-product/products-list-table', [ 'products' => $products['products'], 'current_page' => \S::get( 'current_page' ), - 'apilo_enabled' => \admin\factory\Integrations::apilo_settings( 'enabled' ), + 'apilo_enabled' => $integrationsRepository -> getSetting( 'apilo', 'enabled' ), 'show_xml_data' => \S::get_session( 'show_xml_data' ) ] ) ]; @@ -253,6 +257,10 @@ class ShopProduct static public function view_list() { + global $mdb; + + $integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb ); + $current_page = \S::get_session( 'products_list_current_page' ); if ( !$current_page ) { @@ -276,9 +284,9 @@ class ShopProduct 'current_page' => $current_page, 'query_array' => $query_array, 'pagination_max' => ceil( \admin\factory\ShopProduct::count_product() / 10 ), - 'apilo_enabled' => \admin\factory\Integrations::apilo_settings( 'enabled' ), + 'apilo_enabled' => $integrationsRepository -> getSetting( 'apilo', 'enabled' ), 'show_xml_data' => \S::get_session( 'show_xml_data' ), - 'shoppro_enabled' => \admin\factory\Integrations::shoppro_settings( 'enabled' ) + 'shoppro_enabled' => $integrationsRepository -> getSetting( 'shoppro', 'enabled' ) ] ); } diff --git a/autoload/admin/factory/class.Integrations.php b/autoload/admin/factory/class.Integrations.php deleted file mode 100644 index 7c17121..0000000 --- a/autoload/admin/factory/class.Integrations.php +++ /dev/null @@ -1,69 +0,0 @@ -getSetting( 'apilo', $name ) : $repo->getSettings( 'apilo' ); - } - - static public function apilo_settings_save( $field_id, $value ) - { - return self::repo()->saveSetting( 'apilo', $field_id, $value ); - } - - static public function apilo_get_access_token() - { - return self::repo()->apiloGetAccessToken(); - } - - static public function apilo_keepalive( int $refresh_lead_seconds = 300 ) - { - return self::repo()->apiloKeepalive( $refresh_lead_seconds ); - } - - static public function apilo_authorization( $client_id, $client_secret, $authorization_code ) - { - return self::repo()->apiloAuthorize( $client_id, $client_secret, $authorization_code ); - } - - // ── Apilo product linking ───────────────────────────────────── - - static public function apilo_product_select_save( int $product_id, $apilo_product_id, $apilo_product_name ) - { - return self::repo()->linkProduct( $product_id, $apilo_product_id, $apilo_product_name ); - } - - static public function apilo_product_select_delete( int $product_id ) - { - return self::repo()->unlinkProduct( $product_id ); - } - - // ── ShopPRO settings ────────────────────────────────────────── - - static public function shoppro_settings( $name = '' ) - { - $repo = self::repo(); - return $name ? $repo->getSetting( 'shoppro', $name ) : $repo->getSettings( 'shoppro' ); - } - - static public function shoppro_settings_save( $field_id, $value ) - { - return self::repo()->saveSetting( 'shoppro', $field_id, $value ); - } -} diff --git a/autoload/admin/factory/class.ShopOrder.php b/autoload/admin/factory/class.ShopOrder.php deleted file mode 100644 index 27f6138..0000000 --- a/autoload/admin/factory/class.ShopOrder.php +++ /dev/null @@ -1,114 +0,0 @@ - $order['apilo_order_id'], - 'status' => (int)\front\factory\ShopStatuses::get_apilo_status_id( $new_status ) - ] ) ); - curl_setopt( $ch, CURLOPT_HTTPHEADER, [ - "Authorization: Bearer " . $access_token, - "Accept: application/json", - "Content-Type: application/json" - ] ); - curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true); - $apilo_result = curl_exec( $ch ); - - $apilo_result = json_decode( $apilo_result, true ); - - if ( $apilo_result['updates'] == 1 ) { - - // zmień ID zamówienia na największe ID zamówienia + 1, oraz usuń ID zamówienia z apilo - $new_order_id = $mdb -> max( 'pp_shop_orders', 'id' ) + 1; - - // pobierz listę kolumn zamówienia - $query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_orders' AND COLUMN_NAME != 'id'"; - $columns = $mdb -> query( $query ) -> fetchAll( \PDO::FETCH_COLUMN ); - $columns_list = implode( ', ', $columns ); - - // kopiuj stare zamówienie do nowego ID - $mdb -> query( 'INSERT INTO pp_shop_orders (' . $columns_list . ') SELECT ' . $columns_list . ' FROM pp_shop_orders pso WHERE pso.id = ' . $order_id ); - $new_order_id = $mdb -> id(); - - // pobierz listę kolumn produktów zamówienia - $query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_order_products' AND COLUMN_NAME != 'id' AND COLUMN_NAME != 'order_id'"; - $columns = $mdb -> query( $query ) -> fetchAll( \PDO::FETCH_COLUMN ); - $columns_list = implode( ', ', $columns ); - - // kopiuj produkty zamówienia do nowego zamówienia - $mdb -> query( 'INSERT INTO pp_shop_order_products (order_id, ' . $columns_list . ') SELECT ' . $new_order_id . ',' . $columns_list . ' FROM pp_shop_order_products psop WHERE psop.order_id = ' . $order_id ); - - // pobierz listę kolumn z tabeli pp_shop_order_statuses - $query = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'pp_shop_order_statuses' AND COLUMN_NAME != 'id' AND COLUMN_NAME != 'order_id'"; - $columns = $mdb -> query( $query ) -> fetchAll( \PDO::FETCH_COLUMN ); - $columns_list = implode( ', ', $columns ); - - // kopiuj statusy zamówienia do nowego zamówienia - $mdb -> query( 'INSERT INTO pp_shop_order_statuses (order_id, ' . $columns_list . ') SELECT ' . $new_order_id . ',' . $columns_list . ' FROM pp_shop_order_statuses psos WHERE psos.order_id = ' . $order_id ); - - // usuń stare zamówienie - $mdb -> delete( 'pp_shop_orders', [ 'id' => $order_id ] ); - $mdb -> delete( 'pp_shop_order_products', [ 'order_id' => $order_id ] ); - $mdb -> delete( 'pp_shop_order_statuses', [ 'order_id' => $order_id ] ); - - // zmień wartość kolumny apilo_order_id na NULL - $mdb -> update( 'pp_shop_orders', [ 'apilo_order_id' => NULL ], [ 'id' => $new_order_id ] ); - - return true; - } - - curl_close( $ch ); - } - - return false; - } - - static public function next_order_id( int $order_id ) - { - global $mdb; - - if ( !$order_id ) - return false; - - return $mdb -> get( 'pp_shop_orders', 'id', [ 'id[>]' => $order_id, 'ORDER' => [ 'id' => 'ASC' ], 'LIMIT' => 1 ] ); - } - - static public function prev_order_id( int $order_id ) - { - global $mdb; - - if ( !$order_id ) - return false; - - return $mdb -> get( 'pp_shop_orders', 'id', [ 'id[<]' => $order_id, 'ORDER' => [ 'id' => 'DESC' ], 'LIMIT' => 1 ] ); - } - - static public function order_details( int $order_id ) - { - global $mdb; - - $order = $mdb -> get( 'pp_shop_orders', '*', [ 'id' => $order_id ] ); - $order['products'] = $mdb -> select( 'pp_shop_order_products', '*', [ 'order_id' => $order_id ] ); - - return $order; - } -} diff --git a/autoload/shop/class.Order.php b/autoload/shop/class.Order.php index 251f4be..2ea5e40 100644 --- a/autoload/shop/class.Order.php +++ b/autoload/shop/class.Order.php @@ -55,10 +55,9 @@ class Order implements \ArrayAccess public static function order_statuses() { global $mdb; - $results = $mdb -> select( 'pp_shop_statuses', [ 'id', 'status' ], [ 'ORDER' => [ 'o' => 'ASC' ] ] ); - if ( \S::is_array_fix( $results ) ) foreach ( $results as $row ) - $statuses[ (int)$row['id'] ] = $row['status']; - return $statuses; + + $repository = new \Domain\ShopStatus\ShopStatusRepository( $mdb ); + return $repository -> allStatuses(); } public function update_aplio_order_status_date( $date ) @@ -80,8 +79,10 @@ class Order implements \ArrayAccess { global $mdb, $config; + $integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb ); + // apilo - $apilo_settings = \admin\factory\Integrations::apilo_settings(); + $apilo_settings = $integrationsRepository -> getSettings( 'apilo' ); if ( $apilo_settings['enabled'] and $apilo_settings['access-token'] and $apilo_settings['sync_orders'] ) { // put data to file @@ -117,6 +118,8 @@ class Order implements \ArrayAccess { global $mdb, $config; + $integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb ); + if ( $this -> status == $status ) return false; @@ -137,7 +140,7 @@ class Order implements \ArrayAccess $response['result'] = true; // apilo - $apilo_settings = \admin\factory\Integrations::apilo_settings(); + $apilo_settings = $integrationsRepository -> getSettings( 'apilo' ); if ( $apilo_settings['enabled'] and $apilo_settings['access-token'] and $apilo_settings['sync_orders'] ) { // put data to file @@ -322,7 +325,9 @@ class Order implements \ArrayAccess private function sync_apilo_payment(): bool { - global $config; + global $config, $mdb; + + $integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb ); if ( !(int)$this -> apilo_order_id ) return true; @@ -332,7 +337,7 @@ class Order implements \ArrayAccess $payment_type = 1; $payment_date = new \DateTime( $this -> date_order ); - $access_token = \admin\factory\Integrations::apilo_get_access_token(); + $access_token = $integrationsRepository -> apiloGetAccessToken(); $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $this -> apilo_order_id . '/payment/' ); @@ -371,12 +376,14 @@ class Order implements \ArrayAccess private function sync_apilo_status( int $status ): bool { - global $config; + global $config, $mdb; + + $integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb ); if ( !(int)$this -> apilo_order_id ) return true; - $access_token = \admin\factory\Integrations::apilo_get_access_token(); + $access_token = $integrationsRepository -> apiloGetAccessToken(); $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, "https://projectpro.apilo.com/rest/api/orders/" . $this -> apilo_order_id . '/status/' ); diff --git a/cron.php b/cron.php index 921d385..c25e7ab 100644 --- a/cron.php +++ b/cron.php @@ -53,12 +53,13 @@ $mdb = new medoo( [ ] ); $settings = \front\factory\Settings::settings_details(); -$apilo_settings = \admin\factory\Integrations::apilo_settings(); +$integrationsRepository = new \Domain\Integrations\IntegrationsRepository( $mdb ); +$apilo_settings = $integrationsRepository -> getSettings( 'apilo' ); // Keepalive tokenu Apilo: odswiezaj token przed wygasnieciem, zeby integracja byla stale aktywna. if ( (int)($apilo_settings['enabled'] ?? 0) === 1 ) { - \admin\factory\Integrations::apilo_keepalive( 300 ); - $apilo_settings = \admin\factory\Integrations::apilo_settings(); + $integrationsRepository -> apiloKeepalive( 300 ); + $apilo_settings = $integrationsRepository -> getSettings( 'apilo' ); Order::process_apilo_sync_queue( 10 ); } @@ -122,7 +123,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_products'] and $apilo_ { if ( $result = $mdb -> query( 'SELECT id, apilo_product_id, apilo_get_data_date, apilo_product_name FROM pp_shop_products WHERE apilo_product_id IS NOT NULL AND apilo_product_id != 0 AND ( apilo_get_data_date IS NULL OR apilo_get_data_date <= \'' . date( 'Y-m-d H:i:s', strtotime( '-10 minutes', time() ) ) . '\' ) ORDER BY apilo_get_data_date ASC LIMIT 1' ) -> fetch( \PDO::FETCH_ASSOC ) ) { - $access_token = \admin\factory\Integrations::apilo_get_access_token(); + $access_token = $integrationsRepository -> apiloGetAccessToken(); $url = 'https://projectpro.apilo.com/rest/api/warehouse/product/' . $result['apilo_product_id'] . '/'; $curl = curl_init( $url ); curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true ); @@ -151,7 +152,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_products'] and $apilo_ // synchronizacja cen apilo.com if ( $apilo_settings['enabled'] and $apilo_settings['access-token'] and ( !$apilo_settings['pricelist_update_date'] or $apilo_settings['pricelist_update_date'] <= date( 'Y-m-d H:i:s', strtotime( '-1 hour', time() ) ) ) ) { - $access_token = \admin\factory\Integrations::apilo_get_access_token(); + $access_token = $integrationsRepository -> apiloGetAccessToken(); $url = 'https://projectpro.apilo.com/rest/api/warehouse/price-calculated/?price=' . $apilo_settings['pricelist_id']; @@ -188,7 +189,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['access-token'] and ( !$apil } } } - \admin\factory\Integrations::apilo_settings_save( 'pricelist_update_date', date( 'Y-m-d H:i:s' ) ); + $integrationsRepository -> saveSetting( 'apilo', 'pricelist_update_date', date( 'Y-m-d H:i:s' ) ); echo '

Zaktualizowałem ceny produktów (APILO)

'; } @@ -253,7 +254,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se 'media' => null ]; - $access_token = \admin\factory\Integrations::apilo_get_access_token(); + $access_token = $integrationsRepository -> apiloGetAccessToken(); $order_date = new DateTime( $order['date_order'] ); @@ -502,7 +503,7 @@ if ( $apilo_settings['enabled'] and $apilo_settings['sync_orders'] and $apilo_se { if ( $order['apilo_order_id'] ) { - $access_token = \admin\factory\Integrations::apilo_get_access_token(); + $access_token = $integrationsRepository -> apiloGetAccessToken(); $url = 'https://projectpro.apilo.com/rest/api/orders/' . $order['apilo_order_id'] . '/'; $ch = curl_init( $url ); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bde710d..58dd8d5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,44 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.277 (2026-02-15) - Stabilizacja ShopOrder + Integrations + Global Search + +- **ShopOrder (stabilizacja po migracji)** + - FIX: `Domain\Order\OrderRepository::listForAdmin()` - poprawa zapytan SQL (count/list), bezpieczne fallbacki i poprawne zwracanie listy zamowien w `/admin/shop_order/list/` + - FIX: wyrównanie wysokości komórek w `components/table-list` (`vertical-align` + lokalny override dla `.text-right` w tabeli) +- **Integrations (cleanup)** + - CLEANUP: usunieta fasada `autoload/admin/factory/class.Integrations.php` + - UPDATE: przepięcie wywołań na `Domain\Integrations\IntegrationsRepository` w: `cron.php`, `shop\Order`, `admin\Controllers\ShopPaymentMethodController`, `admin\Controllers\ShopStatusesController`, `admin\Controllers\ShopTransportController`, `admin\controls\ShopProduct` +- **Admin UX** + - NOWE: globalna wyszukiwarka w top-barze (obok "Wyczysc cache") dla produktow i zamowien + - NOWE: endpoint `/admin/settings/globalSearchAjax/` (`SettingsController::globalSearchAjax`) + - FIX: wsparcie wyszukiwania po pełnym imieniu i nazwisku (np. "Jan Kowalski") + poprawka escapingu SQL w `CONCAT_WS` +- TEST: + - Pelny suite: **OK (385 tests, 1246 assertions)** + - Test punktowy: `SettingsControllerTest` **OK (7 tests, 10 assertions)** + +--- + +## ver. 0.276 (2026-02-15) - ShopOrder + +- **ShopOrder** - migracja `/admin/shop_order/*` na Domain + DI + nowe widoki + - NOWE: `Domain\Order\OrderRepository` (lista admin z filtrowaniem/sortowaniem, szczegóły, historia statusów, notes, save admin, summary, trustmate, delete) + - NOWE: `Domain\Order\OrderAdminService` (operacje aplikacyjne admin: status/paid/unpaid/resend email/send to apilo/delete) + - NOWE: `admin\Controllers\ShopOrderController` (DI) z akcjami `list/view_list`, `details/order_details`, `edit/order_edit`, `save/order_save`, `notes_save`, `order_status_change`, `order_resend_confirmation_email`, `set_order_as_paid`, `set_order_as_unpaid`, `send_order_to_apilo`, `toggle_trustmate_send`, `delete/order_delete` + - UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ShopOrder` + - UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_order/list/` + - UPDATE: lista zamówień przepięta z legacy grid na `components/table-list` (`shop-order/orders-list`) + - UPDATE: `shop-order/order-details` i `shop-order/order-edit` przebudowane bez `gridEdit` + wydzielenie JS do `*-custom-script.php` + - UPDATE: `shop\Order::order_statuses()` przepiete na `Domain\ShopStatus\ShopStatusRepository` + - UPDATE: `admin\controls\Dashboard` pobiera statusy przez `Domain\ShopStatus\ShopStatusRepository` + - CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopOrder.php`, `autoload/admin/factory/class.ShopOrder.php`, `admin/templates/shop-order/view-list.php` +- TEST: + - NOWE: `tests/Unit/Domain/Order/OrderRepositoryTest.php` + - NOWE: `tests/Unit/admin/Controllers/ShopOrderControllerTest.php` + - Testy punktowe: **OK (8 tests, 49 assertions)** + +--- + ## ver. 0.275 (2026-02-15) - ShopCategory - **ShopCategory** - migracja `/admin/shop_category/*` na Domain + DI + nowe endpointy AJAX diff --git a/docs/DATABASE_STRUCTURE.md b/docs/DATABASE_STRUCTURE.md index a717ee7..1d14bd2 100644 --- a/docs/DATABASE_STRUCTURE.md +++ b/docs/DATABASE_STRUCTURE.md @@ -115,6 +115,8 @@ Zamówienia sklepu (źródło danych dla list i szczegółów klientów w panelu **Aktualizacja 2026-02-15 (ver. 0.274):** moduł `/admin/shop_clients/*` korzysta z `Domain\Client\ClientRepository` przez `admin\Controllers\ShopClientsController`. +**Aktualizacja 2026-02-15 (ver. 0.276):** moduł `/admin/shop_order/*` korzysta z `Domain\Order\OrderRepository` przez `admin\Controllers\ShopOrderController`; usunięto legacy `admin\controls\ShopOrder` i `admin\factory\ShopOrder`. + ## pp_banners Banery. diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index f3de582..a25246d 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -253,6 +253,19 @@ autoload/ - `cron.php` automatycznie ponawia zalegle syncy (`Order::process_apilo_sync_queue()`). - `shop\Order::set_as_paid()` wysyla mapowany typ platnosci Apilo (z mapowania metody platnosci), bez stalej wartosci `type`. +**Aktualizacja 2026-02-15 (ver. 0.276):** +- Dodano modul domenowy `Domain/Order/OrderRepository.php`. +- Dodano serwis aplikacyjny `Domain/Order/OrderAdminService.php`. +- Dodano kontroler DI `admin/Controllers/ShopOrderController.php`. +- Modul `/admin/shop_order/*` dziala na nowych widokach (`orders-list`, `order-details`, `order-edit`). +- Usunieto legacy: `autoload/admin/controls/class.ShopOrder.php`, `autoload/admin/factory/class.ShopOrder.php`, `admin/templates/shop-order/view-list.php`. + +**Aktualizacja 2026-02-15 (ver. 0.277):** +- Dodano globalna wyszukiwarke admin w `admin/templates/site/main-layout.php` (produkty + zamowienia). +- Dodano endpoint AJAX `SettingsController::globalSearchAjax()` w `autoload/admin/Controllers/SettingsController.php`. +- Usunieto fasade `autoload/admin/factory/class.Integrations.php`. +- Wywołania integracji przepiete bezposrednio na `Domain/Integrations/IntegrationsRepository.php`. + ### Routing admin (admin\Site::route()) 1. Sprawdź mapę `$newControllers` → utwórz instancję z DI → wywołaj 2. Jeśli nowy kontroler nie istnieje (`class_exists()` = false) → fallback na `admin\controls\` diff --git a/docs/REFACTORING_PLAN.md b/docs/REFACTORING_PLAN.md index 257c0f6..ccaadfa 100644 --- a/docs/REFACTORING_PLAN.md +++ b/docs/REFACTORING_PLAN.md @@ -156,6 +156,7 @@ grep -r "Product::getQuantity" . | 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 | | 27 | ShopCategory | 0.275 | CategoryRepository + DI kontroler + routing, endpointy AJAX (`save_categories_order`, `save_products_order`, `cookie_categories`), cleanup legacy controls/factory/view | +| 28 | ShopOrder | 0.276 | OrderRepository + OrderAdminService + DI kontroler + routing + nowe widoki (`orders-list`, `order-details`, `order-edit`) + cleanup legacy controls/factory/view-list | ### Product - szczegolowy status - ✅ getQuantity (ver. 0.238) @@ -170,15 +171,14 @@ grep -r "Product::getQuantity" . - [ ] getProductImg ### 📋 Do zrobienia -- Order - ShopProduct (factory) ## Kolejność refaktoryzacji (priorytet) -1-27: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport, ShopAttribute, ShopProductSets, ShopProducer, ShopProduct (mass_edit), ShopClients, ShopCategory +1-28: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport, ShopAttribute, ShopProductSets, ShopProducer, ShopProduct (mass_edit), ShopClients, ShopCategory, ShopOrder Nastepne: -28. **Order** +29. **ShopProduct (factory)** ## Form Edit System diff --git a/docs/TESTING.md b/docs/TESTING.md index 2e6d2cc..af9b62a 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,13 @@ Alternatywnie (Git Bash): Ostatnio zweryfikowano: 2026-02-15 ```text -OK (377 tests, 1197 assertions) +OK (385 tests, 1246 assertions) +``` + +Aktualizacja po stabilizacji ShopOrder / Integrations / Global Search (2026-02-15, ver. 0.277): +```text +Pelny suite: OK (385 tests, 1246 assertions) +SettingsControllerTest: OK (7 tests, 10 assertions) ``` Aktualizacja po migracji ShopClients (2026-02-15, ver. 0.274) - testy punktowe: @@ -54,11 +60,18 @@ Pelny suite po migracji ShopCategory (2026-02-15, ver. 0.275): OK (377 tests, 1197 assertions) ``` +Aktualizacja po migracji ShopOrder (2026-02-15, ver. 0.276) - testy punktowe: +```text +OK (8 tests, 49 assertions) +``` + Nowe testy dodane 2026-02-15: - `tests/Unit/Domain/Client/ClientRepositoryTest.php` - `tests/Unit/admin/Controllers/ShopClientsControllerTest.php` - `tests/Unit/Domain/Category/CategoryRepositoryTest.php` - `tests/Unit/admin/Controllers/ShopCategoryControllerTest.php` +- `tests/Unit/Domain/Order/OrderRepositoryTest.php` +- `tests/Unit/admin/Controllers/ShopOrderControllerTest.php` ## Struktura testow diff --git a/tests/Unit/Domain/Order/OrderRepositoryTest.php b/tests/Unit/Domain/Order/OrderRepositoryTest.php new file mode 100644 index 0000000..31fc212 --- /dev/null +++ b/tests/Unit/Domain/Order/OrderRepositoryTest.php @@ -0,0 +1,94 @@ +createMock(\medoo::class); + $mockDb->method('select') + ->willReturnCallback(function ($table, $columns, $where) { + if ($table === 'pp_shop_statuses') { + return [ + ['id' => 0, 'status' => 'Nowe'], + ['id' => 4, 'status' => 'W realizacji'], + ]; + } + + return []; + }); + + $repository = new OrderRepository($mockDb); + $statuses = $repository->orderStatuses(); + + $this->assertIsArray($statuses); + $this->assertSame('Nowe', $statuses[0]); + $this->assertSame('W realizacji', $statuses[4]); + } + + public function testNextAndPrevOrderIdReturnNullForInvalidInput(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new OrderRepository($mockDb); + + $this->assertNull($repository->nextOrderId(0)); + $this->assertNull($repository->prevOrderId(0)); + } + + public function testListForAdminReturnsItemsAndTotal(): void + { + $mockDb = $this->createMock(\medoo::class); + + $resultSetCount = new class { + public function fetchAll(): array + { + return [[3]]; + } + }; + + $resultSetData = new class { + public function fetchAll(): array + { + return [ + [ + 'id' => 11, + 'number' => '2026/02/001', + 'date_order' => '2026-02-15 10:00:00', + 'client' => 'Jan Kowalski', + 'order_email' => 'jan@example.com', + 'address' => 'Testowa 1, 00-000 Warszawa', + 'status' => 0, + 'client_phone' => '111222333', + 'transport' => 'Kurier', + 'payment_method' => 'Przelew', + 'summary' => '123.45', + 'paid' => 1, + 'total_orders' => 2, + ], + ]; + } + }; + + $callIndex = 0; + $mockDb->method('query') + ->willReturnCallback(function () use (&$callIndex, $resultSetCount, $resultSetData) { + $callIndex++; + return $callIndex === 1 ? $resultSetCount : $resultSetData; + }); + + $repository = new OrderRepository($mockDb); + $result = $repository->listForAdmin([], 'date_order', 'DESC', 1, 15); + + $this->assertSame(3, $result['total']); + $this->assertCount(1, $result['items']); + $this->assertSame(11, $result['items'][0]['id']); + $this->assertSame('2026/02/001', $result['items'][0]['number']); + $this->assertSame(2, $result['items'][0]['total_orders']); + $this->assertSame(1, $result['items'][0]['paid']); + } +} diff --git a/tests/Unit/admin/Controllers/ShopOrderControllerTest.php b/tests/Unit/admin/Controllers/ShopOrderControllerTest.php new file mode 100644 index 0000000..f055c68 --- /dev/null +++ b/tests/Unit/admin/Controllers/ShopOrderControllerTest.php @@ -0,0 +1,84 @@ +service = $this->createMock(OrderAdminService::class); + $this->controller = new ShopOrderController($this->service); + } + + public function testConstructorAcceptsService(): void + { + $controller = new ShopOrderController($this->service); + $this->assertInstanceOf(ShopOrderController::class, $controller); + } + + public function testHasExpectedActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'list')); + $this->assertTrue(method_exists($this->controller, 'view_list')); + $this->assertTrue(method_exists($this->controller, 'details')); + $this->assertTrue(method_exists($this->controller, 'order_details')); + $this->assertTrue(method_exists($this->controller, 'edit')); + $this->assertTrue(method_exists($this->controller, 'order_edit')); + $this->assertTrue(method_exists($this->controller, 'save')); + $this->assertTrue(method_exists($this->controller, 'order_save')); + $this->assertTrue(method_exists($this->controller, 'notes_save')); + $this->assertTrue(method_exists($this->controller, 'order_status_change')); + $this->assertTrue(method_exists($this->controller, 'order_resend_confirmation_email')); + $this->assertTrue(method_exists($this->controller, 'set_order_as_unpaid')); + $this->assertTrue(method_exists($this->controller, 'set_order_as_paid')); + $this->assertTrue(method_exists($this->controller, 'send_order_to_apilo')); + $this->assertTrue(method_exists($this->controller, 'toggle_trustmate_send')); + $this->assertTrue(method_exists($this->controller, 'delete')); + $this->assertTrue(method_exists($this->controller, 'order_delete')); + } + + public function testViewActionsReturnString(): 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('order_details')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('edit')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('order_edit')->getReturnType()); + } + + public function testMutationActionsReturnVoid(): void + { + $reflection = new \ReflectionClass($this->controller); + + $this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('order_save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('notes_save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('order_status_change')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('order_resend_confirmation_email')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('set_order_as_unpaid')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('set_order_as_paid')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('send_order_to_apilo')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('toggle_trustmate_send')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('delete')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('order_delete')->getReturnType()); + } + + public function testConstructorRequiresOrderAdminService(): void + { + $reflection = new \ReflectionClass(ShopOrderController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(1, $params); + $this->assertEquals('Domain\\Order\\OrderAdminService', $params[0]->getType()->getName()); + } +} diff --git a/updates/0.20/ver_0.276.zip b/updates/0.20/ver_0.276.zip new file mode 100644 index 0000000..850cab2 Binary files /dev/null and b/updates/0.20/ver_0.276.zip differ diff --git a/updates/0.20/ver_0.276_files.txt b/updates/0.20/ver_0.276_files.txt new file mode 100644 index 0000000..93f4965 --- /dev/null +++ b/updates/0.20/ver_0.276_files.txt @@ -0,0 +1,4 @@ +F: ../admin/templates/shop-order/view-list.php +F: ../autoload/admin/controls/class.ShopOrder.php +F: ../autoload/admin/factory/class.Integrations.php +F: ../autoload/admin/factory/class.ShopOrder.php diff --git a/updates/changelog.php b/updates/changelog.php index a890c09..8919b54 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,14 @@ +ver. 0.276 - 15.02.2026
+- NEW - migracja modulu `ShopOrder` do architektury Domain + DI (`Domain\Order\OrderRepository`, `Domain\Order\OrderAdminService`, `admin\Controllers\ShopOrderController`) +- UPDATE - modul `/admin/shop_order/*` przepiety na nowy routing (kanoniczny URL `/admin/shop_order/list/`) i nowe widoki (`orders-list`, `order-details`, `order-edit`) +- FIX - stabilizacja listy zamowien (`OrderRepository::listForAdmin`) oraz poprawa wygladu tabeli (`components/table-list`, wyrownanie komorek i `text-right`) +- CLEANUP - usuniete legacy klasy/pliki: `autoload/admin/controls/class.ShopOrder.php`, `autoload/admin/factory/class.ShopOrder.php`, `admin/templates/shop-order/view-list.php` +- UPDATE - usunieta fasada `autoload/admin/factory/class.Integrations.php`; wywolania przepiete na `Domain\Integrations\IntegrationsRepository` +- NEW - globalna wyszukiwarka admin (produkty + zamowienia) przy "Wyczysc cache" + endpoint `/admin/settings/globalSearchAjax/` +- FIX - wyszukiwanie po pelnym imieniu i nazwisku w global search +- UPDATE - testy: `OK (385 tests, 1246 assertions)` +- UPDATE - pliki aktualizacji: `updates/0.20/ver_0.276.zip`, `ver_0.276_files.txt` +
ver. 0.275 - 15.02.2026
- NEW - migracja modulu `ShopCategory` do architektury Domain + DI (`Domain\Category\CategoryRepository`, `admin\Controllers\ShopCategoryController`) - UPDATE - modul `/admin/shop_category/*` przepiety na nowy routing (kanoniczny URL `/admin/shop_category/list/`) i endpointy AJAX kontrolera (`save_categories_order`, `save_products_order`, `cookie_categories`) diff --git a/updates/versions.php b/updates/versions.php index bf31f67..176ae77 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@