'Προτεινόμενα (Optisleek)'] + (array) $options; } public static function default_catalog_orderby($default) { return 'optisleek'; } public static function catalog_ordering_args($args) { if (self::current_orderby() === 'optisleek') { // Βάση: η default σειρά του WooCommerce — όπου υπάρχει ενεργός // κανόνας, το posts_clauses (apply_sorting) την αντικαθιστά. $args['orderby'] = 'menu_order title'; $args['order'] = 'ASC'; unset($args['meta_key']); } return $args; } // Η τρέχουσα επιλογή ταξινόμησης: ρητή από το URL, αλλιώς η προεπιλογή. private static function current_orderby(): string { // Δημόσια παράμετρος ταξινόμησης του καταλόγου (read-only). if (isset($_GET['orderby'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return sanitize_text_field(wp_unslash($_GET['orderby'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } // Το core filter του WooCommerce, ώστε να ισχύουν και άλλα plugins/themes. return (string) apply_filters( 'woocommerce_default_catalog_orderby', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound get_option('woocommerce_default_catalog_orderby', 'menu_order') ); } public static function schedule_cron(): void { if (!wp_next_scheduled('optisleek_sorting_cron_sync')) { wp_schedule_event(time(), 'twicedaily', 'optisleek_sorting_cron_sync'); } } public static function unschedule_cron(): void { $timestamp = wp_next_scheduled('optisleek_sorting_cron_sync'); if ($timestamp) { wp_unschedule_event($timestamp, 'optisleek_sorting_cron_sync'); } } public static function handle_cron_sync(): void { $result = self::sync_from_tool(); self::write_log($result, 'cron'); } public static function sync_from_tool(): array { $base_url = rtrim(get_option('optisleek_url', ''), '/'); $token = get_option('api_token', ''); $shop_id = get_option('optisleek_shop_id', ''); if (!$base_url || !$token || !$shop_id) { return ['success' => false, 'message' => 'Λείπουν ρυθμίσεις (URL, token, ή Shop ID).']; } $response = wp_remote_post("{$base_url}/api/products/sort", [ 'headers' => ['Content-Type' => 'application/json'], 'body' => wp_json_encode(['api_token' => $token, 'shop_id' => $shop_id]), 'timeout' => 30, 'sslverify' => false, ]); if (is_wp_error($response)) { return ['success' => false, 'message' => $response->get_error_message()]; } $status = wp_remote_retrieve_response_code($response); if ($status !== 200) { return ['success' => false, 'message' => "HTTP {$status}"]; } $data = json_decode(wp_remote_retrieve_body($response), true); if (empty($data['success']) || !isset($data['products'])) { return ['success' => false, 'message' => $data['message'] ?? 'Αποτυχία από το tool.']; } $synced = 0; foreach ((array) $data['products'] as $category_group) { if (!is_array($category_group) || empty($category_group)) continue; usort($category_group, fn($a, $b) => ($a['position'] ?? 0) <=> ($b['position'] ?? 0)); $cat_id = (int) ($category_group[0]['category_foreign_key'] ?? 0); if (!$cat_id) continue; $product_ids = []; $seen = []; foreach ($category_group as $item) { $pid = (int) ($item['product_foreign_key'] ?? 0); if ($pid && !isset($seen[$pid])) { $product_ids[] = $pid; $seen[$pid] = true; } } if (empty($product_ids)) continue; $existing = self::find_rule_by_single_category($cat_id); if ($existing) { self::update($existing->id, [ 'product_ids' => wp_json_encode(array_values($product_ids)), ]); } else { self::create([$cat_id], $product_ids, false); } $synced++; } return ['success' => true, 'synced' => $synced]; } public static function write_log(array $result, string $trigger): void { $log = get_option('optisleek_sorting_log', []); array_unshift($log, [ 'time' => current_time('Y-m-d H:i:s'), 'success' => $result['success'], 'synced' => $result['synced'] ?? 0, 'message' => $result['message'] ?? '', 'trigger' => $trigger, ]); update_option('optisleek_sorting_log', array_slice($log, 0, 50), false); } public static function get_log(): array { return get_option('optisleek_sorting_log', []); } public static function clear_log(): void { delete_option('optisleek_sorting_log'); } private static function find_rule_by_single_category(int $cat_id): ?object { global $wpdb; // Ίδια σειρά προτεραιότητας με το find_rule_for_category(), ώστε ο // συγχρονισμός να ενημερώνει τον ίδιο κανόνα που όντως εφαρμόζεται. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom πίνακας του plugin $rules = $wpdb->get_results( $wpdb->prepare('SELECT * FROM %i ORDER BY updated_at DESC, id DESC', self::table()) ) ?: []; foreach ($rules as $rule) { $ids = json_decode($rule->category_ids, true); if (is_array($ids) && in_array($cat_id, array_map('intval', $ids), true)) { return $rule; } } return null; } // ── Table ───────────────────────────────────────────────────────────────── public static function table(): string { global $wpdb; return $wpdb->prefix . 'optisleek_sorting'; } public static function maybe_create_table(): void { if (get_option(self::DB_VERSION_OPTION) === self::DB_VERSION) return; global $wpdb; $table = self::table(); $charset = $wpdb->get_charset_collate(); $sql = "CREATE TABLE {$table} ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, category_ids text NOT NULL, product_ids longtext NOT NULL, enabled tinyint(1) NOT NULL DEFAULT 1, updated_at datetime NOT NULL, PRIMARY KEY (id) ) {$charset};"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); update_option(self::DB_VERSION_OPTION, self::DB_VERSION); } // ── CRUD (χρησιμοποιείται από REST API + Admin) ─────────────────────────── public static function get_all(): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom πίνακας του plugin return $wpdb->get_results( $wpdb->prepare('SELECT * FROM %i ORDER BY id DESC', self::table()) ) ?: []; } public static function get_by_id(int $id): ?object { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom πίνακας του plugin return $wpdb->get_row( $wpdb->prepare('SELECT * FROM %i WHERE id = %d', self::table(), $id) ) ?: null; } public static function create(array $category_ids, array $product_ids, bool $enabled = true): int { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- custom πίνακας του plugin $wpdb->insert(self::table(), [ 'category_ids' => wp_json_encode(array_values(array_map('intval', $category_ids))), 'product_ids' => wp_json_encode(array_values(array_map('intval', $product_ids))), 'enabled' => $enabled ? 1 : 0, 'updated_at' => current_time('mysql'), ]); return (int) $wpdb->insert_id; } public static function update(int $id, array $fields): bool { global $wpdb; $fields['updated_at'] = current_time('mysql'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom πίνακας του plugin return (bool) $wpdb->update(self::table(), $fields, ['id' => $id]); } public static function delete(int $id): bool { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom πίνακας του plugin return (bool) $wpdb->delete(self::table(), ['id' => $id], ['%d']); } // ── Sorting Logic ───────────────────────────────────────────────────────── public static function apply_sorting($clauses, $query) { if (!function_exists('is_woocommerce') || !is_woocommerce()) return $clauses; if (!$query->is_main_query()) return $clauses; // Σεβασμός στην επιλογή του πελάτη: η Optisleek σειρά εφαρμόζεται ΜΟΝΟ // όταν ισχύει το «Προτεινόμενα (Optisleek)» (προεπιλογή ή ρητή επιλογή). if (self::current_orderby() !== 'optisleek') return $clauses; $cat_id = self::current_category_id($query); if (!$cat_id) return $clauses; $rule = self::find_rule_for_category($cat_id); if (!$rule) return $clauses; $ids = json_decode($rule->product_ids, true); if (empty($ids) || !is_array($ids)) return $clauses; $ids = array_map('intval', $ids); $ids_csv = implode(',', $ids); global $wpdb; // Tiebreaker: τα προϊόντα εκτός λίστας παίρνουν σταθερή δευτερεύουσα // σειρά (menu_order → ημερομηνία → ID), αλλιώς η MySQL δεν εγγυάται // συνεπή σειρά μεταξύ σελίδων (διπλοεμφανίσεις / παραλείψεις στο pagination). $clauses['orderby'] = "FIELD({$wpdb->posts}.ID, {$ids_csv}) = 0, " . "FIELD({$wpdb->posts}.ID, {$ids_csv}) ASC, " . "{$wpdb->posts}.menu_order ASC, {$wpdb->posts}.post_date DESC, {$wpdb->posts}.ID DESC"; return $clauses; } private static function current_category_id(WP_Query $query): int { // Direct category page (/shop/poukamisa/) $obj = get_queried_object(); if ($obj instanceof WP_Term && $obj->taxonomy === 'product_cat') { return (int) $obj->term_id; } // Filtered shop — διαβάζει από το tax_query (plugin-agnostic) if ( $query->tax_query && ! empty( $query->tax_query->queries ) ) { foreach ( $query->tax_query->queries as $tq ) { if ( isset( $tq['taxonomy'] ) && $tq['taxonomy'] === 'product_cat' && ! empty( $tq['terms'] ) && count( (array) $tq['terms'] ) === 1 ) { $terms = (array) $tq['terms']; $field = $tq['field'] ?? 'term_id'; if ( $field === 'term_id' ) { return (int) $terms[0]; } $term = get_term_by( $field, $terms[0], 'product_cat' ); if ( $term && ! is_wp_error( $term ) ) { return (int) $term->term_id; } } } } // AJAX filter plugin — διαβάζει filter_cat από GET (slug-based, standard WC convention) // Δημόσια παράμετρος φίλτρου του καταλόγου (read-only). if ( ! empty( $_GET['filter_cat'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $slug = sanitize_text_field( wp_unslash( $_GET['filter_cat'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $term = get_term_by( 'slug', $slug, 'product_cat' ); if ( $term && ! is_wp_error( $term ) ) { return (int) $term->term_id; } } return 0; } private static function find_rule_for_category(int $cat_id): ?object { global $wpdb; // ORDER BY: αν υπάρχουν πολλοί ενεργοί κανόνες για την ίδια κατηγορία // (το POST /sorting δημιουργεί νέο κανόνα σε κάθε Submit), κερδίζει ο // πιο πρόσφατος. Χωρίς ORDER BY η MySQL επέστρεφε συνήθως τον παλιότερο, // οπότε το site έδειχνε ξεπερασμένη λίστα. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom πίνακας του plugin $rules = $wpdb->get_results( $wpdb->prepare('SELECT * FROM %i WHERE enabled = 1 ORDER BY updated_at DESC, id DESC', self::table()) ) ?: []; foreach ($rules as $rule) { $ids = json_decode($rule->category_ids, true); if (is_array($ids) && in_array($cat_id, array_map('intval', $ids), true)) { return $rule; } } return null; } }