489 lines
14 KiB
PHP
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,
|
|
];
|
|
}
|