34849-vm/includes/agregados_excel_import.php
2026-07-10 04:54:44 +00:00

489 lines
14 KiB
PHP

<?php
// Importación de pedidos "Agregados" desde Excel (.xlsx) o CSV.
// Objetivo: cargar pedidos manuales sin romper la secuencia de pedidos provenientes de Drive.
function cc_agregados_null_if_empty(mixed $value): mixed
{
if (is_string($value)) {
$value = trim($value);
return $value === '' ? null : $value;
}
return $value === '' ? null : $value;
}
function cc_agregados_col_letters_to_index(string $letters): int
{
$letters = strtoupper(trim($letters));
if ($letters === '') {
return 0;
}
$index = 0;
$len = strlen($letters);
for ($i = 0; $i < $len; $i++) {
$ch = $letters[$i];
if ($ch < 'A' || $ch > '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,
];
}