'Z') { continue; } $index = $index * 26 + (ord($ch) - ord('A') + 1); } return max(0, $index - 1); } function cc_agregados_normalize_header(string $label): string { $label = trim($label); if ($label === '') { return ''; } $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT', $label); if ($ascii !== false && $ascii !== '') { $label = $ascii; } $label = mb_strtoupper($label); $label = preg_replace('/[^A-Z0-9]+/', '', $label); return (string) ($label ?? ''); } function cc_agregados_guess_field_from_header(string $normalizedHeader): ?string { if ($normalizedHeader === '') { return null; } $h = $normalizedHeader; if (str_contains($h, 'NOMBRE') || str_contains($h, 'CLIENTE')) { return 'nombre'; } if (str_contains($h, 'CELULAR') || str_contains($h, 'TELEF')) { return 'celular'; } if (str_contains($h, 'DNI')) { return 'dni'; } if (str_contains($h, 'DIREC')) { return 'direccion'; } if (str_contains($h, 'REFER')) { return 'referencia'; } if (str_contains($h, 'SEDE')) { return 'sede'; } if (str_contains($h, 'CIUDAD')) { return 'ciudad'; } if (str_contains($h, 'DISTRIT')) { return 'distrito'; } if (str_contains($h, 'PRODUCT')) { return 'producto'; } if (str_contains($h, 'CANT')) { return 'cantidad'; } if (str_contains($h, 'PRECIO')) { return 'precio'; } if (str_contains($h, 'PAIS')) { return 'pais'; } if (str_contains($h, 'COORD')) { return 'coordenadas'; } if (str_contains($h, 'METODO')) { return 'metodo'; } if (str_contains($h, 'OBSERV')) { return 'observaciones'; } return null; } function cc_agregados_read_csv_rows(string $filePath): array { $handle = fopen($filePath, 'rb'); if (!$handle) { throw new RuntimeException('No se pudo leer el archivo CSV.'); } $firstLine = fgets($handle); if ($firstLine === false) { fclose($handle); return []; } $delimiter = ';'; $commaCount = substr_count($firstLine, ','); $semiCount = substr_count($firstLine, ';'); $delimiter = $semiCount >= $commaCount ? ';' : ','; rewind($handle); $rows = []; $rowNum = 1; while (($line = fgetcsv($handle, 0, $delimiter)) !== false) { // fgetcsv puede devolver nulls. $clean = []; foreach ($line as $cell) { $clean[] = $cell === null ? '' : (string) $cell; } $rows[$rowNum] = $clean; $rowNum++; } fclose($handle); return $rows; } function cc_agregados_read_xlsx_rows(string $filePath): array { $zip = new ZipArchive(); $openResult = $zip->open($filePath); if ($openResult !== true) { throw new RuntimeException('No se pudo abrir el archivo XLSX.'); } $sharedStrings = []; $sharedName = 'xl/sharedStrings.xml'; if ($zip->locateName($sharedName) !== false) { $sharedXml = $zip->getFromName($sharedName); if ($sharedXml) { $doc = new DOMDocument(); $doc->loadXML($sharedXml); $xpath = new DOMXPath($doc); foreach ($xpath->query("//*[local-name()='si']") as $siNode) { $parts = []; foreach ($xpath->query(".//*[local-name()='t']", $siNode) as $tNode) { $parts[] = (string) $tNode->textContent; } $sharedStrings[] = implode('', $parts); } } } // Elegimos la primera hoja. $sheetName = null; if ($zip->locateName('xl/worksheets/sheet1.xml') !== false) { $sheetName = 'xl/worksheets/sheet1.xml'; } else { $candidates = []; for ($i = 0; $i < $zip->numFiles; $i++) { $name = $zip->getNameIndex($i); if (preg_match('#^xl/worksheets/sheet\d+\.xml$#', $name)) { $candidates[] = $name; } } sort($candidates); $sheetName = $candidates[0] ?? null; } if (!$sheetName) { throw new RuntimeException('No se encontró una hoja en el archivo XLSX.'); } $sheetXml = $zip->getFromName($sheetName); if (!$sheetXml) { throw new RuntimeException('No se pudo leer la hoja del archivo XLSX.'); } $doc = new DOMDocument(); $doc->loadXML($sheetXml); $xpath = new DOMXPath($doc); $rows = []; $rowNodes = $xpath->query("//*[local-name()='sheetData']/*[local-name()='row']"); foreach ($rowNodes as $rowNode) { $rowNumRaw = $rowNode->getAttribute('r'); $rowNum = (int) $rowNumRaw; if ($rowNum <= 0) { continue; } foreach ($xpath->query("*[local-name()='c']", $rowNode) as $cNode) { /** @var DOMElement $cNode */ $cellRef = $cNode->getAttribute('r'); if ($cellRef === '') { continue; } $colLetters = preg_replace('/\d+/', '', $cellRef); $colIndex = cc_agregados_col_letters_to_index($colLetters); if ($colIndex < 0) { continue; } $cellType = $cNode->getAttribute('t'); $value = ''; if ($cellType === 's') { $v = (string) $xpath->evaluate("string(./*[local-name()='v'])", $cNode); $idx = (int) $v; $value = $sharedStrings[$idx] ?? ''; } elseif ($cellType === 'inlineStr') { $parts = []; foreach ($xpath->query("./*[local-name()='is']/*[local-name()='t']", $cNode) as $tNode) { $parts[] = (string) $tNode->textContent; } $value = implode('', $parts); } else { $value = (string) $xpath->evaluate("string(./*[local-name()='v'])", $cNode); } $rows[$rowNum][$colIndex] = $value; } } return $rows; } function cc_agregados_normalize_phone(string $value): string { $value = trim($value); if ($value === '') { return ''; } return preg_replace('/\D+/', '', $value); } function cc_agregados_import_from_uploaded_file(PDO $pdo, string $storeKey, array $uploadedFile): array { if (!isset($uploadedFile['error']) || (int) $uploadedFile['error'] !== UPLOAD_ERR_OK) { $err = (int) ($uploadedFile['error'] ?? -1); throw new RuntimeException('Error al subir el archivo. Código: ' . $err); } $tmpPath = (string) ($uploadedFile['tmp_name'] ?? ''); if ($tmpPath === '' || !file_exists($tmpPath)) { throw new RuntimeException('El archivo temporal no está disponible.'); } $originalName = (string) ($uploadedFile['name'] ?? ''); $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); // Ensure schema. drive_test_ensure_orders_table($pdo); $rowsByIndex = []; if ($ext === 'xlsx') { $rowsByIndex = cc_agregados_read_xlsx_rows($tmpPath); } elseif ($ext === 'csv') { $rowsByIndex = cc_agregados_read_csv_rows($tmpPath); } else { throw new RuntimeException('Formato no soportado. Solo .xlsx o .csv'); } if (empty($rowsByIndex)) { throw new RuntimeException('El archivo está vacío o no se pudo leer.'); } // Cabecera: intentamos fila 1. $rowNumbers = array_keys($rowsByIndex); sort($rowNumbers); $headerRowNum = $rowNumbers[0] ?? 1; if (isset($rowsByIndex[1])) { $headerRowNum = 1; } $headerRow = $rowsByIndex[$headerRowNum] ?? []; if (!is_array($headerRow) || empty($headerRow)) { throw new RuntimeException('No se encontró la fila de encabezados.'); } // Column index => field key $colToField = []; foreach ($headerRow as $colIndex => $headerValue) { $headerValue = trim((string) $headerValue); if ($headerValue === '') { continue; } $normalized = cc_agregados_normalize_header($headerValue); $field = cc_agregados_guess_field_from_header($normalized); if ($field !== null) { $colToField[(int) $colIndex] = $field; } } if (empty($colToField)) { throw new RuntimeException('No se reconocieron encabezados. Revisa la plantilla de Excel.'); } $maxRowNum = max($rowNumbers); $now = date('Y-m-d H:i:s'); $stmt = $pdo->prepare( "INSERT INTO callcenter_test_orders ( store_key, source_key, codigo, import_id, drive_imported_at, is_agregado, nombre, direccion_drive, referencia_drive, sede_drive, ciudad_drive, distrito_drive, dni_drive, observaciones_drive, celular, producto, cantidad, precio, pais, coordenadas, metodo ) VALUES ( :store_key, :source_key, :codigo, :import_id, :drive_imported_at, :is_agregado, :nombre, :direccion_drive, :referencia_drive, :sede_drive, :ciudad_drive, :distrito_drive, :dni_drive, :observaciones_drive, :celular, :producto, :cantidad, :precio, :pais, :coordenadas, :metodo ) ON DUPLICATE KEY UPDATE store_key = VALUES(store_key), codigo = VALUES(codigo), import_id = VALUES(import_id), drive_imported_at = VALUES(drive_imported_at), is_agregado = VALUES(is_agregado), nombre = VALUES(nombre), direccion_drive = VALUES(direccion_drive), referencia_drive = VALUES(referencia_drive), sede_drive = VALUES(sede_drive), ciudad_drive = VALUES(ciudad_drive), distrito_drive = VALUES(distrito_drive), dni_drive = VALUES(dni_drive), observaciones_drive = VALUES(observaciones_drive), celular = VALUES(celular), producto = VALUES(producto), cantidad = VALUES(cantidad), precio = VALUES(precio), pais = VALUES(pais), coordenadas = VALUES(coordenadas), metodo = VALUES(metodo), last_seen_at = CURRENT_TIMESTAMP" ); $inserted = 0; $skipped = 0; $rowProcessed = 0; for ($rowNum = $headerRowNum + 1; $rowNum <= $maxRowNum; $rowNum++) { if (!isset($rowsByIndex[$rowNum])) { continue; } $rowProcessed++; $data = []; foreach ($colToField as $colIndex => $field) { $val = $rowsByIndex[$rowNum][$colIndex] ?? ''; $val = trim((string) $val); $data[$field] = $val; } $nombre = $data['nombre'] ?? ''; $celularRaw = $data['celular'] ?? ''; $celular = cc_agregados_normalize_phone($celularRaw); $producto = $data['producto'] ?? ''; $isEmptyRow = ($nombre === '' && $celular === '' && $producto === ''); if ($isEmptyRow) { $skipped++; continue; } $dni = cc_agregados_null_if_empty($data['dni'] ?? ''); $direccion = cc_agregados_null_if_empty($data['direccion'] ?? ''); $referencia = cc_agregados_null_if_empty($data['referencia'] ?? ''); $sede = cc_agregados_null_if_empty($data['sede'] ?? ''); $ciudad = cc_agregados_null_if_empty($data['ciudad'] ?? ''); $distrito = cc_agregados_null_if_empty($data['distrito'] ?? ''); $observaciones = cc_agregados_null_if_empty($data['observaciones'] ?? ''); $cantidad = cc_agregados_null_if_empty($data['cantidad'] ?? ''); $precio = cc_agregados_null_if_empty($data['precio'] ?? ''); $pais = cc_agregados_null_if_empty($data['pais'] ?? ''); $coordenadas = cc_agregados_null_if_empty($data['coordenadas'] ?? ''); $metodo = cc_agregados_null_if_empty($data['metodo'] ?? ''); $sourceKeyBase = implode('|', [ $storeKey, (string) $nombre, (string) $celular, (string) ($dni ?? ''), (string) $producto, (string) ($cantidad ?? ''), (string) ($precio ?? ''), (string) ($direccion ?? ''), (string) ($referencia ?? ''), (string) ($sede ?? ''), (string) ($ciudad ?? ''), (string) ($distrito ?? ''), (string) ($observaciones ?? ''), (string) ($metodo ?? ''), ]); $sourceKey = sha1('manual|' . $sourceKeyBase); $stmt->execute([ ':store_key' => $storeKey, ':source_key' => $sourceKey, ':codigo' => null, ':import_id' => null, ':drive_imported_at' => $now, ':is_agregado' => 1, ':nombre' => cc_agregados_null_if_empty($nombre), ':direccion_drive' => $direccion, ':referencia_drive' => $referencia, ':sede_drive' => $sede, ':ciudad_drive' => $ciudad, ':distrito_drive' => $distrito, ':dni_drive' => $dni, ':observaciones_drive' => $observaciones, ':celular' => $celular !== '' ? $celular : null, ':producto' => cc_agregados_null_if_empty($producto), ':cantidad' => $cantidad, ':precio' => $precio, ':pais' => $pais, ':coordenadas' => $coordenadas, ':metodo' => $metodo, ]); $inserted++; } return [ 'inserted' => $inserted, 'skipped' => $skipped, 'rows_processed' => $rowProcessed, ]; }