build_sample_rows($key_by); $slug = 'stock-import-' . $key_by; if ($format === 'xlsx') { $info = AIEO_DMM_Sample::create_xlsx($slug, $headers, $rows); } else { $info = AIEO_DMM_Sample::create_csv($slug, $headers, $rows); } if (empty($info['url'])) { wp_send_json_error(array('message' => __('Could not create sample file (ZipArchive required for XLSX).', 'ai-eshop-optimizer'))); } wp_send_json_success($info); } private function build_sample_rows($key_by) { if (!function_exists('wc_get_products')) { return array( array($key_by === 'product_id' ? '123' : 'EXAMPLE-SKU-1', '10', __('Example product 1', 'ai-eshop-optimizer')), array($key_by === 'product_id' ? '456' : 'EXAMPLE-SKU-2', '25', __('Example product 2', 'ai-eshop-optimizer')), ); } $products = wc_get_products(array( 'status' => 'publish', 'limit' => 5, 'orderby' => 'date', 'order' => 'DESC', )); $out = array(); foreach ($products as $p) { $key = $key_by === 'product_id' ? (string) $p->get_id() : (string) $p->get_sku(); if ($key === '') { continue; } $stock = (int) ($p->get_stock_quantity() ?? 0); $out[] = array($key, (string) $stock, (string) $p->get_name()); } if (empty($out)) { $out[] = array($key_by === 'product_id' ? '123' : 'EXAMPLE-SKU-1', '10', __('Example product', 'ai-eshop-optimizer')); } return $out; } public function render_subtab() { $this->render_template('stock-import.php', array( 'recent_jobs' => $this->recent_jobs(), )); } public function ajax_upload() { AIEO_DMM::verify_ajax_request(); if (empty($_FILES['csv_file']['tmp_name']) || !is_uploaded_file($_FILES['csv_file']['tmp_name'])) { wp_send_json_error(array('message' => __('No file uploaded.', 'ai-eshop-optimizer'))); } $key_by = isset($_POST['key_by']) && sanitize_key(wp_unslash($_POST['key_by'])) === 'product_id' ? 'product_id' : 'sku'; $update_stock_status = !empty($_POST['update_stock_status']); $raw = (string) file_get_contents($_FILES['csv_file']['tmp_name']); if ($raw === '') { wp_send_json_error(array('message' => __('Cannot read file.', 'ai-eshop-optimizer'))); } $raw = preg_replace('/^\xEF\xBB\xBF/', '', $raw); if (function_exists('mb_check_encoding') && !mb_check_encoding($raw, 'UTF-8')) { if (function_exists('iconv')) { $converted = @iconv('WINDOWS-1253', 'UTF-8//TRANSLIT', $raw); if ($converted !== false) { $raw = $converted; } } elseif (function_exists('mb_convert_encoding')) { $raw = mb_convert_encoding($raw, 'UTF-8', 'Windows-1253'); } } $handle = fopen('php://memory', 'r+'); if ($handle === false) { wp_send_json_error(array('message' => __('Cannot read file.', 'ai-eshop-optimizer'))); } fwrite($handle, $raw); rewind($handle); $first_row = fgetcsv($handle); if (!$first_row) { fclose($handle); wp_send_json_error(array('message' => __('Empty CSV.', 'ai-eshop-optimizer'))); } $first_lower = array_map(static function ($v) { return strtolower(trim((string) $v)); }, $first_row); $looks_like_header = in_array($key_by, $first_lower, true) && in_array('stock', $first_lower, true); if ($looks_like_header) { $header = $first_row; } else { $synth_header = array($key_by, 'stock', 'name'); $header = $synth_header; fclose($handle); $handle = fopen('php://memory', 'r+'); if ($handle === false) { wp_send_json_error(array('message' => __('Cannot read file.', 'ai-eshop-optimizer'))); } $synthetic_csv_line = '"' . implode('","', $synth_header) . '"' . "\n"; fwrite($handle, $synthetic_csv_line); fwrite($handle, $raw); rewind($handle); fgetcsv($handle); } $map = array(); foreach ($header as $i => $col) { $map[strtolower(trim((string) $col))] = $i; } if (!isset($map[$key_by]) || !isset($map['stock'])) { fclose($handle); wp_send_json_error(array('message' => sprintf( __('CSV must contain %s and stock columns.', 'ai-eshop-optimizer'), $key_by ))); } $rows = array(); while (($row = fgetcsv($handle)) !== false) { if (!is_array($row)) { continue; } $key_val = isset($row[$map[$key_by]]) ? trim((string) $row[$map[$key_by]]) : ''; $stock_val_raw = isset($row[$map['stock']]) ? trim((string) $row[$map['stock']]) : ''; if ($key_val === '' || $stock_val_raw === '') { continue; } if (!is_numeric($stock_val_raw)) { continue; } $rows[] = array( 'key' => $key_val, 'stock' => (float) $stock_val_raw, ); } fclose($handle); if (empty($rows)) { wp_send_json_error(array('message' => __('No valid rows in CSV.', 'ai-eshop-optimizer'))); } global $wpdb; $job_id = wp_generate_password(16, false, false); $wpdb->insert($wpdb->prefix . 'aieo_dmm_export_jobs', array( 'job_id' => $job_id, 'type' => 'stock_import', 'status' => 'running', 'total' => count($rows), 'processed' => 0, 'params' => wp_json_encode(array( 'key_by' => $key_by, 'update_stock_status' => $update_stock_status, 'rows' => $rows, 'errors' => array(), 'updated'=> 0, )), 'user_id' => get_current_user_id(), ), array('%s', '%s', '%s', '%d', '%d', '%s', '%d')); wp_send_json_success(array('job_id' => $job_id, 'total' => count($rows))); } public function ajax_process() { AIEO_DMM::verify_ajax_request(); $job_id = isset($_POST['job_id']) ? sanitize_text_field(wp_unslash($_POST['job_id'])) : ''; global $wpdb; $jobs_table = $wpdb->prefix . 'aieo_dmm_export_jobs'; $job = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$jobs_table} WHERE job_id = %s", $job_id), ARRAY_A); if (!$job || $job['type'] !== 'stock_import') { wp_send_json_error(array('message' => __('Invalid job.', 'ai-eshop-optimizer'))); } $params = json_decode($job['params'], true); if (!is_array($params)) { wp_send_json_error(array('message' => __('Corrupted job.', 'ai-eshop-optimizer'))); } $batch_size = 200; $offset = (int) $job['processed']; $slice = array_slice($params['rows'], $offset, $batch_size); $errors = isset($params['errors']) ? (array) $params['errors'] : array(); $updated = isset($params['updated']) ? (int) $params['updated'] : 0; foreach ($slice as $row) { $product_id = 0; if ($params['key_by'] === 'product_id') { $product_id = (int) $row['key']; } else { if (function_exists('wc_get_product_id_by_sku')) { $product_id = (int) wc_get_product_id_by_sku($row['key']); } } if ($product_id <= 0) { $errors[] = sprintf( __('Not found: %s', 'ai-eshop-optimizer'), (string) $row['key']); continue; } if ( ! function_exists('wc_get_product') ) { $errors[] = __('WooCommerce not loaded — stock import requires WC.', 'ai-eshop-optimizer'); break; } $p = wc_get_product($product_id); if ( ! $p ) { $errors[] = sprintf( __('Product %d not found in WC.', 'ai-eshop-optimizer'), $product_id); continue; } try { $p->set_stock_quantity( (int) $row['stock'] ); if ( ! empty($params['update_stock_status']) ) { $p->set_stock_status( $row['stock'] > 0 ? 'instock' : 'outofstock' ); } $p->save(); } catch ( Throwable $e ) { $errors[] = sprintf( __('Failed id %1$d: %2$s', 'ai-eshop-optimizer'), $product_id, $e->getMessage() ); continue; } $updated++; do_action('aieo_dmm_after_import_stock', $product_id, (float) $row['stock'], $job_id); } $new_processed = $offset + count($slice); $complete = $new_processed >= (int) $job['total']; $params['errors'] = array_slice($errors, 0, 500); $params['updated'] = $updated; $wpdb->update($jobs_table, array( 'processed' => $new_processed, 'status' => $complete ? 'complete' : 'running', 'params' => wp_json_encode($params), ), array('job_id' => $job_id), array('%d', '%s', '%s'), array('%s')); wp_send_json_success(array( 'processed' => $new_processed, 'total' => (int) $job['total'], 'updated' => $updated, 'errors' => array_slice($errors, -20), 'complete' => $complete, 'percentage'=> (int) $job['total'] > 0 ? (int) floor(($new_processed / (int) $job['total']) * 100) : 100, )); } private function recent_jobs() { global $wpdb; $table = $wpdb->prefix . 'aieo_dmm_export_jobs'; if (!aieo_dmm_table_exists($table)) { return array(); } $rows = $wpdb->get_results( "SELECT * FROM {$table} WHERE type = 'stock_import' ORDER BY created_at DESC LIMIT 10", ARRAY_A ); return is_array($rows) ? $rows : array(); } }