$filters * @return array{items:array>, total:int, page:int, per_page:int, error:string} */ public function paginate(array $filters): array { $page = max(1, (int) ($filters['page'] ?? 1)); $perPage = max(1, min(100, (int) ($filters['per_page'] ?? 20))); $offset = ($page - 1) * $perPage; $where = []; $params = []; $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm'); $effectiveOrderedAtSql = $this->effectiveOrderedAtSql('o'); $search = trim((string) ($filters['search'] ?? '')); if ($search !== '') { $where[] = '(' . 'o.source_order_id LIKE :search ' . 'OR o.external_order_id LIKE :search ' . 'OR o.customer_login LIKE :search ' . 'OR a.name LIKE :search ' . 'OR a.email LIKE :search' . ')'; $params['search'] = '%' . $search . '%'; } $source = trim((string) ($filters['source'] ?? '')); if ($source !== '') { $where[] = 'o.source = :source'; $params['source'] = $source; } $status = trim((string) ($filters['status'] ?? '')); if ($status !== '') { $where[] = $effectiveStatusSql . ' = :status'; $params['status'] = $status; } $paymentStatus = trim((string) ($filters['payment_status'] ?? '')); if ($paymentStatus !== '' && ctype_digit($paymentStatus)) { $where[] = 'o.payment_status = :payment_status'; $params['payment_status'] = (int) $paymentStatus; } $dateFrom = trim((string) ($filters['date_from'] ?? '')); if ($dateFrom !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom) === 1) { $where[] = $effectiveOrderedAtSql . ' >= :date_from'; $params['date_from'] = $dateFrom . ' 00:00:00'; } $dateTo = trim((string) ($filters['date_to'] ?? '')); if ($dateTo !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo) === 1) { $where[] = $effectiveOrderedAtSql . ' <= :date_to'; $params['date_to'] = $dateTo . ' 23:59:59'; } $whereSql = $where === [] ? '' : (' WHERE ' . implode(' AND ', $where)); $sort = (string) ($filters['sort'] ?? 'ordered_at'); $sortDir = strtoupper((string) ($filters['sort_dir'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; $sortColumn = match ($sort) { 'source_order_id' => 'o.source_order_id', 'external_order_id' => 'o.external_order_id', 'external_status_id' => 'o.external_status_id', 'payment_status' => 'o.payment_status', 'total_with_tax' => 'o.total_with_tax', 'total_paid' => 'o.total_paid', 'source_updated_at' => 'o.source_updated_at', 'fetched_at' => 'o.fetched_at', 'id' => 'o.id', default => $effectiveOrderedAtSql, }; try { $countSql = 'SELECT COUNT(*) FROM orders o ' . 'LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer" ' . 'LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code' . $whereSql; $countStmt = $this->pdo->prepare($countSql); $countStmt->execute($params); $total = (int) $countStmt->fetchColumn(); $listSql = 'SELECT o.id, o.internal_order_number, o.source, o.source_order_id, o.external_order_id, o.external_status_id, ' . $effectiveStatusSql . ' AS effective_status_id, o.payment_status, o.currency, o.total_with_tax, o.total_paid, o.ordered_at, o.source_created_at, o.source_updated_at, o.fetched_at, ' . $effectiveOrderedAtSql . ' AS effective_ordered_at, o.is_invoice, o.is_canceled_by_buyer, a.name AS buyer_name, a.email AS buyer_email, a.city AS buyer_city, o.external_carrier_id, o.external_payment_type_id, (SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS items_count, (SELECT COALESCE(SUM(oi.quantity), 0) FROM order_items oi WHERE oi.order_id = o.id) AS items_qty, (SELECT COUNT(*) FROM order_shipments sh WHERE sh.order_id = o.id) AS shipments_count, (SELECT COUNT(*) FROM order_documents od WHERE od.order_id = o.id) AS documents_count FROM orders o LEFT JOIN order_addresses a ON a.order_id = o.id AND a.address_type = "customer" LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code' . $whereSql . ' ORDER BY ' . $sortColumn . ' ' . $sortDir . ' LIMIT :limit OFFSET :offset'; $stmt = $this->pdo->prepare($listSql); foreach ($params as $key => $value) { if (is_int($value)) { $stmt->bindValue(':' . $key, $value, PDO::PARAM_INT); } else { $stmt->bindValue(':' . $key, $value); } } $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); $rows = $stmt->fetchAll(); if (!is_array($rows)) { $rows = []; } $itemPreviewsByOrderId = $this->loadOrderItemsPreviews(array_map( static fn (array $row): int => (int) ($row['id'] ?? 0), $rows )); return [ 'items' => array_map(static function (array $row) use ($itemPreviewsByOrderId): array { $orderId = (int) ($row['id'] ?? 0); return [ 'id' => $orderId, 'internal_order_number' => (string) ($row['internal_order_number'] ?? ''), 'source' => (string) ($row['source'] ?? ''), 'source_order_id' => (string) ($row['source_order_id'] ?? ''), 'external_order_id' => (string) ($row['external_order_id'] ?? ''), 'external_status_id' => (string) ($row['external_status_id'] ?? ''), 'effective_status_id' => (string) ($row['effective_status_id'] ?? ''), 'payment_status' => isset($row['payment_status']) ? (int) $row['payment_status'] : null, 'currency' => (string) ($row['currency'] ?? ''), 'total_with_tax' => $row['total_with_tax'] !== null ? (float) $row['total_with_tax'] : null, 'total_paid' => $row['total_paid'] !== null ? (float) $row['total_paid'] : null, 'ordered_at' => (string) ($row['effective_ordered_at'] ?? ''), 'source_created_at' => (string) ($row['source_created_at'] ?? ''), 'source_updated_at' => (string) ($row['source_updated_at'] ?? ''), 'fetched_at' => (string) ($row['fetched_at'] ?? ''), 'is_invoice' => (int) ($row['is_invoice'] ?? 0) === 1, 'is_canceled_by_buyer' => (int) ($row['is_canceled_by_buyer'] ?? 0) === 1, 'external_carrier_id' => (string) ($row['external_carrier_id'] ?? ''), 'external_payment_type_id' => (string) ($row['external_payment_type_id'] ?? ''), 'buyer_name' => (string) ($row['buyer_name'] ?? ''), 'buyer_email' => (string) ($row['buyer_email'] ?? ''), 'buyer_city' => (string) ($row['buyer_city'] ?? ''), 'items_count' => (int) ($row['items_count'] ?? 0), 'items_qty' => (float) ($row['items_qty'] ?? 0), 'shipments_count' => (int) ($row['shipments_count'] ?? 0), 'documents_count' => (int) ($row['documents_count'] ?? 0), 'items_preview' => (array) ($itemPreviewsByOrderId[$orderId] ?? []), ]; }, $rows), 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'error' => '', ]; } catch (Throwable $exception) { return [ 'items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage, 'error' => $exception->getMessage(), ]; } } /** * @return array */ public function statusOptions(): array { try { $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm'); $rows = $this->pdo->query( 'SELECT DISTINCT ' . $effectiveStatusSql . ' AS effective_status_id FROM orders o LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code WHERE ' . $effectiveStatusSql . ' IS NOT NULL AND ' . $effectiveStatusSql . ' <> "" ORDER BY effective_status_id ASC' )->fetchAll(PDO::FETCH_COLUMN); } catch (Throwable) { return []; } if (!is_array($rows)) { return []; } $options = []; foreach ($rows as $row) { $value = trim((string) $row); if ($value === '') { continue; } $options[$value] = $value; } return $options; } /** * @return array */ public function sourceOptions(): array { try { $rows = $this->pdo->query('SELECT DISTINCT source FROM orders WHERE source IS NOT NULL AND source <> "" ORDER BY source ASC')->fetchAll(PDO::FETCH_COLUMN); } catch (Throwable) { return []; } if (!is_array($rows)) { return []; } $options = []; foreach ($rows as $row) { $value = trim((string) $row); if ($value === '') { continue; } $options[$value] = $value; } return $options; } /** * @return array{all:int, paid:int, shipped:int} */ public function quickStats(): array { try { $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm'); $row = $this->pdo->query('SELECT COUNT(*) AS all_count, SUM(CASE WHEN payment_status = 2 THEN 1 ELSE 0 END) AS paid_count, SUM(CASE WHEN ' . $effectiveStatusSql . ' IN ("shipped", "delivered", "returned") THEN 1 ELSE 0 END) AS shipped_count FROM orders o LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code')->fetch(PDO::FETCH_ASSOC); } catch (Throwable) { return [ 'all' => 0, 'paid' => 0, 'shipped' => 0, ]; } if (!is_array($row)) { return [ 'all' => 0, 'paid' => 0, 'shipped' => 0, ]; } return [ 'all' => (int) ($row['all_count'] ?? 0), 'paid' => (int) ($row['paid_count'] ?? 0), 'shipped' => (int) ($row['shipped_count'] ?? 0), ]; } /** * @return array */ public function statusCounts(): array { try { $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm'); $rows = $this->pdo->query( 'SELECT ' . $effectiveStatusSql . ' AS effective_status_id, COUNT(*) AS cnt FROM orders o LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code GROUP BY effective_status_id' )->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable) { return []; } if (!is_array($rows)) { return []; } $result = []; foreach ($rows as $row) { $key = trim((string) ($row['effective_status_id'] ?? '')); if ($key === '') { $key = '_empty'; } $result[$key] = (int) ($row['cnt'] ?? 0); } return $result; } /** * @return array}> */ public function statusPanelConfig(): array { try { $sql = 'SELECT g.id AS group_id, g.name AS group_name, g.color_hex AS group_color_hex, g.sort_order AS group_sort_order, s.code AS status_code, s.name AS status_name, s.sort_order AS status_sort_order FROM order_status_groups g LEFT JOIN order_statuses s ON s.group_id = g.id AND s.is_active = 1 WHERE g.is_active = 1 ORDER BY g.sort_order ASC, g.id ASC, s.sort_order ASC, s.id ASC'; $rows = $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable) { return []; } if (!is_array($rows) || $rows === []) { return []; } $groupMap = []; foreach ($rows as $row) { $groupId = (int) ($row['group_id'] ?? 0); if ($groupId <= 0) { continue; } if (!isset($groupMap[$groupId])) { $groupMap[$groupId] = [ 'name' => trim((string) ($row['group_name'] ?? '')), 'color_hex' => $this->normalizeColorHex((string) ($row['group_color_hex'] ?? '#64748b')), 'items' => [], ]; } $statusCode = trim((string) ($row['status_code'] ?? '')); if ($statusCode === '') { continue; } $groupMap[$groupId]['items'][] = [ 'code' => $statusCode, 'name' => trim((string) ($row['status_name'] ?? $statusCode)), ]; } return array_values($groupMap); } /** * @return array|null */ public function findDetails(int $orderId): ?array { if ($orderId <= 0) { return null; } try { $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm'); $orderStmt = $this->pdo->prepare( 'SELECT o.*, ' . $effectiveStatusSql . ' AS effective_status_id FROM orders o LEFT JOIN allegro_order_status_mappings asm ON o.source = "allegro" AND LOWER(o.external_status_id) = asm.allegro_status_code WHERE o.id = :id LIMIT 1' ); $orderStmt->execute(['id' => $orderId]); $order = $orderStmt->fetch(PDO::FETCH_ASSOC); if (!is_array($order)) { return null; } $addressesStmt = $this->pdo->prepare('SELECT * FROM order_addresses WHERE order_id = :order_id ORDER BY FIELD(address_type, "customer", "invoice", "delivery"), id ASC'); $addressesStmt->execute(['order_id' => $orderId]); $addresses = $addressesStmt->fetchAll(PDO::FETCH_ASSOC); if (!is_array($addresses)) { $addresses = []; } $itemsMediaSql = $this->resolvedMediaUrlSql('oi', 'o.source'); $itemsStmt = $this->pdo->prepare('SELECT oi.*, ' . $itemsMediaSql . ' AS resolved_media_url FROM order_items oi INNER JOIN orders o ON o.id = oi.order_id WHERE oi.order_id = :order_id ORDER BY oi.sort_order ASC, oi.id ASC'); $itemsStmt->execute(['order_id' => $orderId]); $items = $itemsStmt->fetchAll(PDO::FETCH_ASSOC); if (!is_array($items)) { $items = []; } $items = array_map(static function (array $row): array { $resolvedMediaUrl = trim((string) ($row['resolved_media_url'] ?? '')); if ($resolvedMediaUrl !== '') { $row['media_url'] = $resolvedMediaUrl; } unset($row['resolved_media_url']); return $row; }, $items); $paymentsStmt = $this->pdo->prepare('SELECT * FROM order_payments WHERE order_id = :order_id ORDER BY payment_date ASC, id ASC'); $paymentsStmt->execute(['order_id' => $orderId]); $payments = $paymentsStmt->fetchAll(PDO::FETCH_ASSOC); if (!is_array($payments)) { $payments = []; } $shipmentsStmt = $this->pdo->prepare('SELECT * FROM order_shipments WHERE order_id = :order_id ORDER BY posted_at ASC, id ASC'); $shipmentsStmt->execute(['order_id' => $orderId]); $shipments = $shipmentsStmt->fetchAll(PDO::FETCH_ASSOC); if (!is_array($shipments)) { $shipments = []; } $documentsStmt = $this->pdo->prepare('SELECT * FROM order_documents WHERE order_id = :order_id ORDER BY source_created_at ASC, id ASC'); $documentsStmt->execute(['order_id' => $orderId]); $documents = $documentsStmt->fetchAll(PDO::FETCH_ASSOC); if (!is_array($documents)) { $documents = []; } $notesStmt = $this->pdo->prepare('SELECT * FROM order_notes WHERE order_id = :order_id ORDER BY created_at_external DESC, id DESC'); $notesStmt->execute(['order_id' => $orderId]); $notes = $notesStmt->fetchAll(PDO::FETCH_ASSOC); if (!is_array($notes)) { $notes = []; } $historyStmt = $this->pdo->prepare('SELECT * FROM order_status_history WHERE order_id = :order_id ORDER BY changed_at DESC, id DESC'); $historyStmt->execute(['order_id' => $orderId]); $history = $historyStmt->fetchAll(PDO::FETCH_ASSOC); if (!is_array($history)) { $history = []; } $activityLog = $this->loadActivityLog($orderId); return [ 'order' => $order, 'addresses' => $addresses, 'items' => $items, 'payments' => $payments, 'shipments' => $shipments, 'documents' => $documents, 'notes' => $notes, 'status_history' => $history, 'activity_log' => $activityLog, ]; } catch (Throwable) { return null; } } /** * @param array $orderIds * @return array> */ private function loadOrderItemsPreviews(array $orderIds): array { $cleanIds = []; foreach ($orderIds as $id) { $orderId = max(0, (int) $id); if ($orderId <= 0 || in_array($orderId, $cleanIds, true)) { continue; } $cleanIds[] = $orderId; } if ($cleanIds === []) { return []; } $placeholders = implode(',', array_fill(0, count($cleanIds), '?')); try { $resolvedMediaSql = $this->resolvedMediaUrlSql('oi', 'o.source'); $sql = 'SELECT oi.order_id, oi.original_name, oi.quantity, ' . $resolvedMediaSql . ' AS media_url, oi.sort_order, oi.id FROM order_items oi INNER JOIN orders o ON o.id = oi.order_id WHERE oi.order_id IN (' . $placeholders . ') ORDER BY oi.order_id ASC, oi.sort_order ASC, oi.id ASC'; $stmt = $this->pdo->prepare($sql); foreach ($cleanIds as $index => $orderId) { $stmt->bindValue($index + 1, $orderId, PDO::PARAM_INT); } $stmt->execute(); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (Throwable) { return []; } if (!is_array($rows)) { return []; } $result = []; foreach ($rows as $row) { $orderId = (int) ($row['order_id'] ?? 0); if ($orderId <= 0) { continue; } if (!isset($result[$orderId])) { $result[$orderId] = []; } if (count($result[$orderId]) >= 4) { continue; } $result[$orderId][] = [ 'name' => trim((string) ($row['original_name'] ?? '')), 'quantity' => (float) ($row['quantity'] ?? 0), 'media_url' => trim((string) ($row['media_url'] ?? '')), ]; } return $result; } private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string { return 'CASE WHEN ' . $orderAlias . '.source = "allegro" AND ' . $mappingAlias . '.orderpro_status_code IS NOT NULL AND ' . $mappingAlias . '.orderpro_status_code <> "" THEN ' . $mappingAlias . '.orderpro_status_code ELSE ' . $orderAlias . '.external_status_id END'; } private function effectiveOrderedAtSql(string $orderAlias): string { return 'COALESCE(' . $orderAlias . '.ordered_at, ' . $orderAlias . '.source_created_at, ' . $orderAlias . '.source_updated_at, ' . $orderAlias . '.fetched_at' . ')'; } private function resolvedMediaUrlSql(string $itemAlias, string $sourceAlias = '"allegro"'): string { if (!$this->canResolveMappedMedia()) { return 'COALESCE(NULLIF(TRIM(' . $itemAlias . '.media_url), ""), "")'; } return 'COALESCE( NULLIF(TRIM(' . $itemAlias . '.media_url), ""), ( SELECT NULLIF(TRIM(pi.storage_path), "") FROM product_channel_map pcm INNER JOIN sales_channels sc ON sc.id = pcm.channel_id INNER JOIN product_images pi ON pi.product_id = pcm.product_id WHERE LOWER(sc.code) = LOWER(' . $sourceAlias . ') AND ( pcm.external_product_id = ' . $itemAlias . '.external_item_id OR pcm.external_product_id = ' . $itemAlias . '.source_product_id ) ORDER BY pi.is_main DESC, pi.sort_order ASC, pi.id ASC LIMIT 1 ), "" )'; } private function canResolveMappedMedia(): bool { if ($this->supportsMappedMedia !== null) { return $this->supportsMappedMedia; } try { $requiredColumns = [ ['table' => 'product_channel_map', 'column' => 'product_id'], ['table' => 'product_channel_map', 'column' => 'channel_id'], ['table' => 'product_channel_map', 'column' => 'external_product_id'], ['table' => 'sales_channels', 'column' => 'id'], ['table' => 'sales_channels', 'column' => 'code'], ['table' => 'product_images', 'column' => 'id'], ['table' => 'product_images', 'column' => 'product_id'], ['table' => 'product_images', 'column' => 'storage_path'], ['table' => 'product_images', 'column' => 'sort_order'], ['table' => 'product_images', 'column' => 'is_main'], ]; $pairsSql = []; $params = []; foreach ($requiredColumns as $index => $required) { $tableParam = ':table_' . $index; $columnParam = ':column_' . $index; $pairsSql[] = '(TABLE_NAME = ' . $tableParam . ' AND COLUMN_NAME = ' . $columnParam . ')'; $params['table_' . $index] = $required['table']; $params['column_' . $index] = $required['column']; } $sql = 'SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND (' . implode(' OR ', $pairsSql) . ')'; $stmt = $this->pdo->prepare($sql); $stmt->execute($params); $count = (int) $stmt->fetchColumn(); $this->supportsMappedMedia = ($count === count($requiredColumns)); } catch (Throwable) { $this->supportsMappedMedia = false; } return $this->supportsMappedMedia; } /** * @return array> */ private function loadActivityLog(int $orderId): array { try { $stmt = $this->pdo->prepare( 'SELECT * FROM order_activity_log WHERE order_id = :order_id ORDER BY created_at DESC, id DESC' ); $stmt->execute(['order_id' => $orderId]); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); return is_array($rows) ? $rows : []; } catch (Throwable) { return []; } } /** * @param array|null $details */ public function recordActivity( int $orderId, string $eventType, string $summary, ?array $details = null, string $actorType = 'system', ?string $actorName = null ): void { $stmt = $this->pdo->prepare( 'INSERT INTO order_activity_log (order_id, event_type, summary, details_json, actor_type, actor_name, created_at) VALUES (:order_id, :event_type, :summary, :details_json, :actor_type, :actor_name, NOW())' ); $stmt->execute([ 'order_id' => $orderId, 'event_type' => $eventType, 'summary' => $summary, 'details_json' => $details !== null ? json_encode($details, JSON_UNESCAPED_UNICODE) : null, 'actor_type' => $actorType, 'actor_name' => $actorName, ]); } public function recordStatusChange( int $orderId, ?string $fromStatus, string $toStatus, string $changeSource = 'manual', ?string $comment = null, string $actorType = 'system', ?string $actorName = null ): void { $stmt = $this->pdo->prepare( 'INSERT INTO order_status_history (order_id, from_status_id, to_status_id, changed_at, change_source, comment) VALUES (:order_id, :from_status_id, :to_status_id, NOW(), :change_source, :comment)' ); $stmt->execute([ 'order_id' => $orderId, 'from_status_id' => $fromStatus, 'to_status_id' => $toStatus, 'change_source' => $changeSource, 'comment' => $comment, ]); $fromLabel = $fromStatus !== null ? $this->resolveStatusName($fromStatus) : '-'; $toLabel = $this->resolveStatusName($toStatus); $summary = 'Zmiana statusu: ' . $fromLabel . ' → ' . $toLabel; $this->recordActivity($orderId, 'status_change', $summary, [ 'from_status' => $fromStatus, 'to_status' => $toStatus, 'change_source' => $changeSource, 'comment' => $comment, ], $actorType, $actorName); } public function updateOrderStatus(int $orderId, string $newStatusCode, string $actorType = 'user', ?string $actorName = null): bool { try { $stmt = $this->pdo->prepare('SELECT external_status_id FROM orders WHERE id = :id LIMIT 1'); $stmt->execute(['id' => $orderId]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!is_array($row)) { return false; } $oldStatus = trim((string) ($row['external_status_id'] ?? '')); $update = $this->pdo->prepare('UPDATE orders SET external_status_id = :status, updated_at = NOW() WHERE id = :id'); $update->execute(['status' => $newStatusCode, 'id' => $orderId]); $this->recordStatusChange( $orderId, $oldStatus !== '' ? $oldStatus : null, $newStatusCode, 'manual', null, $actorType, $actorName ); return true; } catch (Throwable) { return false; } } private function resolveStatusName(string $code): string { $normalized = strtolower(trim($code)); if ($normalized === '') { return $code; } try { $stmt = $this->pdo->prepare('SELECT name FROM order_statuses WHERE LOWER(code) = :code LIMIT 1'); $stmt->execute(['code' => $normalized]); $name = $stmt->fetchColumn(); if (is_string($name) && trim($name) !== '') { return trim($name); } } catch (Throwable) { } return $code; } private function normalizeColorHex(string $value): string { $trimmed = trim($value); if (preg_match('/^#[0-9a-fA-F]{6}$/', $trimmed) === 1) { return strtolower($trimmed); } return '#64748b'; } }