Autosave: 20260710-045546

This commit is contained in:
Flatlogic Bot 2026-07-10 04:54:44 +00:00
parent 36dde6307a
commit 3c0975c71c
12 changed files with 1147 additions and 25 deletions

View File

@ -73,6 +73,10 @@ function cc_test_display_value(?string $value, string $fallback = 'No registrado
function cc_test_order_label(array $order): string
{
if (!empty($order['is_agregado'])) {
return 'AGREGADOS';
}
$codigo = trim((string) ($order['codigo'] ?? ''));
if ($codigo !== '') {
return '#' . ltrim($codigo, '#');
@ -98,16 +102,40 @@ function cc_test_badge_class(string $estado): string
function cc_test_order_time(array $order): int
{
foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado', 'import_id'] as $field) {
foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado'] as $field) {
$date = cc_test_parse_datetime($order[$field] ?? null);
if ($date) {
return $date->getTimestamp();
}
}
return 0;
// Fallback recency for Drive imports where import_id is not a datetime (e.g. "#35898").
return cc_test_import_time($order);
}
function cc_test_import_time(array $order): int
{
if (!empty($order['is_agregado'])) {
return (int) ($order['id'] ?? 0);
}
$importRaw = trim((string) ($order['import_id'] ?? ''));
if ($importRaw !== '') {
$date = cc_test_parse_datetime($importRaw);
if ($date) {
return $date->getTimestamp();
}
$digits = preg_replace('/\D+/', '', $importRaw);
if ($digits !== '') {
return (int) $digits;
}
}
$codigoRaw = trim((string) ($order['codigo'] ?? ''));
$digits = preg_replace('/\D+/', '', $codigoRaw);
return $digits !== '' ? (int) $digits : 0;
}
function cc_test_followup_semaforo(array $order): ?array
{
if (cc_test_normalize_state((string) ($order['estado'] ?? '')) !== 'SE ENVIO NUMERO DE CUENTA') {
@ -146,6 +174,8 @@ $orders = [];
$visibleOrders = [];
$modalsHtml = [];
$totalRows = 0;
$agregadosCount = 0;
$tuaniLastProcessedRow = null;
$loadLimit = $storeKey === 'otra_tienda' ? 0 : 100;
$startRow = (int) ($storeConfig['startRow'] ?? 5552);
$catalogoProductos = [];
@ -172,9 +202,31 @@ try {
$catalogoProductos = [];
}
$preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey);
$totalRows = (int) ($preview['total_rows'] ?? 0);
$orders = $preview['orders'] ?? [];
if ($storeKey === 'otra_tienda') {
$syncInfo = drive_test_sync_orders_incremental($pdo, $storeKey, 0);
$totalRows = (int) ($syncInfo['drive_rows_total'] ?? 0);
$tuaniLastProcessedRow = (int) ($syncInfo['last_processed_row'] ?? 0);
$startRow = (int) ($syncInfo['next_start_row'] ?? $startRow);
$orders = drive_test_fetch_orders_from_db($pdo, $storeKey);
$agregadosCount = 0;
foreach ($orders as $o) {
if (!empty($o['is_agregado'])) {
$agregadosCount++;
}
}
} else {
$preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey);
$totalRows = (int) ($preview['total_rows'] ?? 0);
$orders = $preview['orders'] ?? [];
$agregadosOrders = drive_test_fetch_orders_from_db($pdo, $storeKey, true);
$agregadosCount = count($agregadosOrders);
if (!empty($agregadosOrders)) {
$orders = array_merge($orders, $agregadosOrders);
}
}
$tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key'));
$orders = drive_test_merge_tracking($orders, $tracking);
@ -289,9 +341,22 @@ try {
}));
usort($visibleOrders, static function (array $a, array $b) use ($view): int {
if ($view === 'pendientes_hoy') {
$aDue = cc_test_parse_datetime($a['proxima_llamada_at'] ?? null);
$bDue = cc_test_parse_datetime($b['proxima_llamada_at'] ?? null);
$aAgregado = !empty($a['is_agregado']);
$bAgregado = !empty($b['is_agregado']);
if ($aAgregado !== $bAgregado) {
return $aAgregado ? 1 : -1;
}
$aImport = cc_test_import_time($a);
$bImport = cc_test_import_time($b);
if ($aImport !== $bImport) {
return $bImport <=> $aImport;
}
if ($view === "pendientes_hoy") {
$aDue = cc_test_parse_datetime($a["proxima_llamada_at"] ?? null);
$bDue = cc_test_parse_datetime($b["proxima_llamada_at"] ?? null);
if ($aDue && $bDue && $aDue != $bDue) {
return $aDue <=> $bDue;
}
@ -334,7 +399,8 @@ require_once 'layout_header.php';
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge rounded-pill text-bg-light border px-3 py-2">Drive detectado: <?php echo (int) $totalRows; ?> filas</span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Extracción: desde fila <?php echo (int) $startRow; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Agregados: <?php echo (int) $agregadosCount; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2"><?php if ($storeKey === 'otra_tienda' && $tuaniLastProcessedRow !== null): ?>TUANI: último procesado <?php echo (int) $tuaniLastProcessedRow; ?> · próxima desde <?php echo (int) $startRow; ?><?php else: ?>Extracción: desde fila <?php echo (int) $startRow; ?><?php endif; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: cada 10 min</span>
<a href="test_importar_drive.php?store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
</div>

View File

@ -0,0 +1,16 @@
-- 088_add_store_key_and_drive_test_checkpoints.sql
-- Adds store_key to callcenter_test_orders (so we can filter per Drive store)
-- and creates a checkpoint table to support incremental Drive imports for TUANI.
ALTER TABLE callcenter_test_orders
ADD COLUMN IF NOT EXISTS store_key VARCHAR(50) NULL;
CREATE TABLE IF NOT EXISTS drive_test_import_checkpoints (
store_key VARCHAR(50) NOT NULL,
last_processed_row INT NOT NULL DEFAULT 0,
last_sync_at DATETIME NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (store_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE INDEX idx_callcenter_test_orders_store_key ON callcenter_test_orders (store_key);

View File

@ -0,0 +1,3 @@
-- 089_expand_callcenter_test_orders_dni_drive.sql
ALTER TABLE callcenter_test_orders
MODIFY COLUMN dni_drive TEXT NULL;

View File

@ -0,0 +1,4 @@
-- 090_expand_callcenter_test_orders_dni_drive_longtext.sql
-- Ensures callcenter_test_orders.dni_drive can store very long values coming from Google Sheets.
ALTER TABLE callcenter_test_orders
MODIFY COLUMN dni_drive LONGTEXT NULL;

View File

@ -0,0 +1,54 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
require_once 'db/config.php';
require_once 'vendor/autoload.php';
use Shuchkin\SimpleXLSXGen;
$headers = [
'NOMBRE',
'CELULAR',
'DNI',
'DIRECCION',
'REFERENCIA',
'SEDE',
'CIUDAD',
'DISTRITO',
'PRODUCTO',
'CANTIDAD',
'PRECIO',
'PAIS',
'COORDENADAS',
'METODO',
'OBSERVACIONES'
];
$data = [
$headers,
[
'Ej: Juan Pérez',
'999999999',
'12345678',
'Av. Siempre Viva 123',
'Referencia (opcional)',
'Sede / ID',
'Ciudad',
'Distrito',
'Producto',
'1',
'10.50',
'Perú',
'lat,long',
'Contraentrega',
'Observaciones (opcional)'
]
];
$filename = 'plantilla_agregados_pedidos.xlsx';
SimpleXLSXGen::fromArray($data, 'Agregados')->downloadAs($filename);

View File

@ -20,7 +20,7 @@ try {
p.producto,
p.cantidad
FROM pedidos p
WHERE p.estado = 'ROTULADO 📦'
WHERE (p.estado = 'ROTULADO 📦' OR p.estado = 'PENDIENTE')
ORDER BY p.id DESC
";
$stmt = $pdo->prepare($query);

View File

@ -13,11 +13,11 @@ try {
$pdo = db();
$type = $_GET['type'] ?? 'all';
// Filtramos solo los pedidos que están en estado 'ROTULADO 📦' y son de la agencia 'SHALOM'
// Filtramos los pedidos ROTULADO 📦 y PENDIENTE (listados como 'Pedidos Rotulados') y de la agencia 'SHALOM'
$query = "
SELECT p.dni_cliente, p.celular, p.sede_envio, p.agencia, p.producto, p.cantidad
FROM pedidos p
WHERE p.estado = 'ROTULADO 📦'
WHERE (p.estado = 'ROTULADO 📦' OR p.estado = 'PENDIENTE')
AND p.agencia LIKE '%SHALOM%'
";

View File

@ -3,6 +3,7 @@ session_start();
require_once 'db/config.php';
require_once 'includes/callcenter_test_helpers.php';
require_once 'includes/drive_test_orders.php';
require_once 'includes/agregados_excel_import.php';
require_once 'includes/contraentrega_cobertura.php';
$provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento();
@ -73,6 +74,10 @@ function cc_test_display_value(?string $value, string $fallback = 'No registrado
function cc_test_order_label(array $order): string
{
if (!empty($order['is_agregado'])) {
return 'AGREGADOS';
}
$codigo = trim((string) ($order['codigo'] ?? ''));
if ($codigo !== '') {
return '#' . ltrim($codigo, '#');
@ -98,16 +103,40 @@ function cc_test_badge_class(string $estado): string
function cc_test_order_time(array $order): int
{
foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado', 'import_id'] as $field) {
foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado'] as $field) {
$date = cc_test_parse_datetime($order[$field] ?? null);
if ($date) {
return $date->getTimestamp();
}
}
return 0;
// Fallback recency for Drive imports where import_id is not a datetime (e.g. "#35898").
return cc_test_import_time($order);
}
function cc_test_import_time(array $order): int
{
if (!empty($order['is_agregado'])) {
return (int) ($order['id'] ?? 0);
}
$importRaw = trim((string) ($order['import_id'] ?? ''));
if ($importRaw !== '') {
$date = cc_test_parse_datetime($importRaw);
if ($date) {
return $date->getTimestamp();
}
$digits = preg_replace('/\D+/', '', $importRaw);
if ($digits !== '') {
return (int) $digits;
}
}
$codigoRaw = trim((string) ($order['codigo'] ?? ''));
$digits = preg_replace('/\D+/', '', $codigoRaw);
return $digits !== '' ? (int) $digits : 0;
}
function cc_test_followup_semaforo(array $order): ?array
{
if (cc_test_normalize_state((string) ($order['estado'] ?? '')) !== 'SE ENVIO NUMERO DE CUENTA') {
@ -141,11 +170,14 @@ $storeConfig = $availableStores[$storeKey];
$errorMessage = null;
$noticeMessage = null;
$uploadErrorMessage = null;
$assessors = [];
$orders = [];
$visibleOrders = [];
$modalsHtml = [];
$totalRows = 0;
$agregadosCount = 0;
$tuaniLastProcessedRow = null;
$loadLimit = $storeKey === 'otra_tienda' ? 0 : 100;
$cupoObjetivoPorAsesora = 10;
@ -177,9 +209,52 @@ try {
$catalogoProductos = [];
}
$preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey);
$totalRows = (int) ($preview['total_rows'] ?? 0);
$orders = $preview['orders'] ?? [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'upload_agregados_excel') {
try {
$file = $_FILES['agregados_excel'] ?? null;
if (!is_array($file)) {
throw new RuntimeException('No se encontró el archivo de Excel.');
}
$maxSize = 10 * 1024 * 1024; // 10 MB
if ((int) ($file['size'] ?? 0) > $maxSize) {
throw new RuntimeException('El archivo es demasiado grande. Máximo 10 MB.');
}
$res = cc_agregados_import_from_uploaded_file($pdo, $storeKey, $file);
$inserted = (int) ($res['inserted'] ?? 0);
$skipped = (int) ($res['skipped'] ?? 0);
$noticeMessage = 'Importación completada: ' . $inserted . ' pedidos agregados.' . ($skipped > 0 ? ' Filas ignoradas: ' . $skipped . '.' : '');
$uploadErrorMessage = null;
} catch (Throwable $e) {
$uploadErrorMessage = $e->getMessage();
}
}
if ($storeKey === 'otra_tienda') {
$syncInfo = drive_test_sync_orders_incremental($pdo, $storeKey, 0);
$totalRows = (int) ($syncInfo['drive_rows_total'] ?? 0);
$tuaniLastProcessedRow = (int) ($syncInfo['last_processed_row'] ?? 0);
$startRow = (int) ($syncInfo['next_start_row'] ?? $startRow);
$orders = drive_test_fetch_orders_from_db($pdo, $storeKey);
$agregadosCount = 0;
foreach ($orders as $o) {
if (!empty($o['is_agregado'])) {
$agregadosCount++;
}
}
} else {
$preview = drive_test_fetch_orders($loadLimit, $startRow, $storeKey);
$totalRows = (int) ($preview['total_rows'] ?? 0);
$orders = $preview['orders'] ?? [];
$agregadosOrders = drive_test_fetch_orders_from_db($pdo, $storeKey, true);
$agregadosCount = count($agregadosOrders);
if (!empty($agregadosOrders)) {
$orders = array_merge($orders, $agregadosOrders);
}
}
$tracking = drive_test_fetch_tracking($pdo, array_column($orders, 'source_key'));
$orders = drive_test_merge_tracking($orders, $tracking);
@ -465,9 +540,22 @@ try {
}));
usort($visibleOrders, static function (array $a, array $b) use ($view): int {
if ($view === 'pendientes_hoy') {
$aDue = cc_test_parse_datetime($a['proxima_llamada_at'] ?? null);
$bDue = cc_test_parse_datetime($b['proxima_llamada_at'] ?? null);
$aAgregado = !empty($a['is_agregado']);
$bAgregado = !empty($b['is_agregado']);
if ($aAgregado !== $bAgregado) {
return $aAgregado ? 1 : -1;
}
$aImport = cc_test_import_time($a);
$bImport = cc_test_import_time($b);
if ($aImport !== $bImport) {
return $bImport <=> $aImport;
}
if ($view === "pendientes_hoy") {
$aDue = cc_test_parse_datetime($a["proxima_llamada_at"] ?? null);
$bDue = cc_test_parse_datetime($b["proxima_llamada_at"] ?? null);
if ($aDue && $bDue && $aDue != $bDue) {
return $aDue <=> $bDue;
}
@ -508,7 +596,8 @@ require_once 'layout_header.php';
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge rounded-pill text-bg-light border px-3 py-2">Drive detectado: <?php echo (int) $totalRows; ?> filas</span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Extracción: desde fila <?php echo (int) $startRow; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Agregados: <?php echo (int) $agregadosCount; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2"><?php if ($storeKey === 'otra_tienda' && $tuaniLastProcessedRow !== null): ?>TUANI: último procesado <?php echo (int) $tuaniLastProcessedRow; ?> · próxima desde <?php echo (int) $startRow; ?><?php else: ?>Extracción: desde fila <?php echo (int) $startRow; ?><?php endif; ?></span>
<span class="badge rounded-pill text-bg-light border px-3 py-2">Auto actualización: cada 10 min</span>
<a href="test_importar_drive.php?store=<?php echo htmlspecialchars($storeKey); ?>" class="btn btn-outline-primary btn-sm">Ver vista previa Drive</a>
</div>
@ -526,6 +615,47 @@ require_once 'layout_header.php';
<?php echo htmlspecialchars($errorMessage); ?>
</section>
<?php else: ?>
<section class="mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3">
<div>
<h2 class="h5 fw-bold mb-1">
<i class="bi bi-upload text-primary"></i> Agregar pedidos por Excel
</h2>
<div class="text-muted small">
Para: <strong><?php echo $storeKey === 'otra_tienda' ? 'TUANI' : 'Flower'; ?></strong>.
Los pedidos cargados se muestran como <strong>AGREGADOS</strong> y no afectan la secuencia de Drive.
</div>
<div class="mt-2">
<a href="download_agregados_pedidos_template.php" class="btn btn-outline-primary btn-sm">Descargar plantilla Excel</a>
</div>
</div>
<div class="w-100 w-lg-auto">
<form method="post" enctype="multipart/form-data" class="d-flex flex-column gap-2">
<input type="hidden" name="action" value="upload_agregados_excel">
<input
type="file"
name="agregados_excel"
class="form-control form-control-sm"
accept=".xlsx,.csv"
required
>
<button type="submit" class="btn btn-primary btn-sm w-100">Importar pedidos</button>
</form>
</div>
</div>
<?php if (!empty($uploadErrorMessage)): ?>
<div class="alert alert-danger mt-3 mb-0" role="alert">
<?php echo htmlspecialchars($uploadErrorMessage); ?>
</div>
<?php endif; ?>
</div>
</div>
</section>
<section class="row g-3 mb-4">
<div class="col-md-6 col-xl-2">
<a href="?view=pendientes_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">

View File

@ -25,7 +25,7 @@ $is_today = isset($_GET['today']);
$sql = "SELECT p.*
FROM pedidos p
WHERE p.estado = 'ROTULADO 📦'";
WHERE (p.estado = 'ROTULADO 📦' OR p.estado = 'PENDIENTE')";
$params = [];
if ($user_role === 'Asesor') {
@ -197,7 +197,7 @@ $label_generated_date = date('d/m');
<?php if (empty($pedidos)): ?>
<div style="text-align: center; margin-top: 50px;">
<h3>No hay pedidos rotulados para mostrar.</h3>
<h3>No hay pedidos rotulados o pendientes para mostrar (ROTULADO 📦 / PENDIENTE).</h3>
</div>
<?php endif; ?>

View File

@ -0,0 +1,488 @@
<?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,
];
}

View File

@ -169,7 +169,7 @@ function cc_test_ensure_schema(PDO $pdo): void
`sede_drive` VARCHAR(120) NULL,
`ciudad_drive` VARCHAR(120) NULL,
`distrito_drive` VARCHAR(120) NULL,
`dni_drive` VARCHAR(40) NULL,
`dni_drive` LONGTEXT NULL,
`observaciones_drive` TEXT NULL,
`celular` VARCHAR(40) DEFAULT NULL,
`producto` TEXT NULL,
@ -185,6 +185,26 @@ function cc_test_ensure_schema(PDO $pdo): void
KEY `idx_callcenter_test_orders_last_seen_at` (`last_seen_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
// Asegurar que `dni_drive` soporte valores grandes (evita SQLSTATE[22001] por truncamiento).
try {
$colStmt = $pdo->prepare("
SELECT DATA_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'callcenter_test_orders'
AND COLUMN_NAME = 'dni_drive'
LIMIT 1
");
$colStmt->execute();
$dataType = strtolower((string) $colStmt->fetchColumn());
if ($dataType !== 'longtext') {
$pdo->exec("ALTER TABLE `callcenter_test_orders` MODIFY COLUMN `dni_drive` LONGTEXT NULL");
}
} catch (Throwable $exception) {
error_log('cc_test_ensure_schema: ' . $exception->getMessage());
}
$pdo->exec("CREATE TABLE IF NOT EXISTS `callcenter_test_tracking` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`source_key` CHAR(40) NOT NULL,

View File

@ -141,7 +141,11 @@ function drive_test_fetch_orders(int $limit = 10, int $startRow = 5552, string $
$dataRows = array_slice($values, 1);
$startIndex = max(0, $startRow - 2);
if ($startIndex > 0 && $startIndex < count($dataRows)) {
$totalDataRows = count($dataRows);
if ($startIndex >= $totalDataRows) {
$dataRows = [];
} elseif ($startIndex > 0) {
$dataRows = array_slice($dataRows, $startIndex);
}
@ -263,3 +267,340 @@ function drive_test_merge_tracking(array $orders, array $tracking): array
return $orders;
}
function drive_test_parse_datetime_mysql(?string $value): ?string
{
$value = trim((string) $value);
if ($value === "") {
return null;
}
$formats = [
"Y-m-d\\TH:i:sP",
"Y-m-d\\TH:i:s",
"Y-m-d\\TH:i",
"Y-m-d H:i:s",
"Y-m-d H:i",
DateTimeInterface::ATOM,
"Y-m-d",
];
foreach ($formats as $format) {
try {
$date = DateTimeImmutable::createFromFormat($format, $value);
if ($date instanceof DateTimeImmutable) {
return $date->format("Y-m-d H:i:s");
}
} catch (Throwable $exception) {
// ignore and try next format
}
}
try {
return (new DateTimeImmutable($value))->format("Y-m-d H:i:s");
} catch (Throwable $exception) {
return null;
}
}
function drive_test_ensure_orders_table(PDO $pdo): void
{
static $checked = false;
if ($checked) {
return;
}
$pdo->exec("CREATE TABLE IF NOT EXISTS `callcenter_test_orders` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`store_key` VARCHAR(50) NULL,
`source_key` CHAR(40) NOT NULL,
`codigo` VARCHAR(80) DEFAULT NULL,
`import_id` VARCHAR(120) DEFAULT NULL,
`is_agregado` TINYINT(1) NOT NULL DEFAULT 0,
`drive_imported_at` DATETIME NULL,
`nombre` VARCHAR(255) DEFAULT NULL,
`direccion_drive` TEXT NULL,
`referencia_drive` TEXT NULL,
`sede_drive` VARCHAR(120) NULL,
`ciudad_drive` VARCHAR(120) NULL,
`distrito_drive` VARCHAR(120) NULL,
`dni_drive` LONGTEXT NULL,
`observaciones_drive` TEXT NULL,
`celular` VARCHAR(40) DEFAULT NULL,
`producto` TEXT NULL,
`cantidad` VARCHAR(60) NULL,
`precio` VARCHAR(60) NULL,
`pais` VARCHAR(80) NULL,
`coordenadas` VARCHAR(255) NULL,
`metodo` VARCHAR(120) NULL,
`first_seen_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_seen_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uniq_callcenter_test_orders_source_key` (`source_key`),
KEY `idx_callcenter_test_orders_drive_imported_at` (`drive_imported_at`),
KEY `idx_callcenter_test_orders_last_seen_at` (`last_seen_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
// In case the existing table was created before we introduced store_key.
cc_test_ensure_column($pdo, "callcenter_test_orders", "store_key", "VARCHAR(50) NULL");
// Asegura columna para pedidos cargados manualmente (Agregados)
cc_test_ensure_column($pdo, "callcenter_test_orders", "is_agregado", "TINYINT(1) NOT NULL DEFAULT 0");
// Asegurar que `dni_drive` soporte valores grandes desde Google Sheets.
try {
$colStmt = $pdo->prepare("
SELECT DATA_TYPE
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'callcenter_test_orders'
AND COLUMN_NAME = 'dni_drive'
LIMIT 1
");
$colStmt->execute();
$dataType = strtolower((string) $colStmt->fetchColumn());
if ($dataType !== 'longtext') {
$pdo->exec("ALTER TABLE `callcenter_test_orders` MODIFY COLUMN `dni_drive` LONGTEXT NULL");
}
} catch (Throwable $exception) {
// No bloqueamos el panel si por alguna razón no se puede ajustar el tipo.
error_log('drive_test_ensure_orders_table: ' . $exception->getMessage());
}
$checked = true;
}
function drive_test_ensure_import_checkpoints_table(PDO $pdo): void
{
static $checked = false;
if ($checked) {
return;
}
$pdo->exec("CREATE TABLE IF NOT EXISTS `drive_test_import_checkpoints` (
`store_key` VARCHAR(50) NOT NULL,
`last_processed_row` INT NOT NULL DEFAULT 0,
`last_sync_at` DATETIME NULL,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`store_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
$checked = true;
}
function drive_test_sync_orders_incremental(PDO $pdo, string $storeKey, int $overlapRows = 50): array
{
$storeKey = mb_strtolower(trim($storeKey));
$storeConfig = drive_test_get_store_config($storeKey);
$baselineStartRow = (int) ($storeConfig["startRow"] ?? 5552);
drive_test_ensure_orders_table($pdo);
drive_test_ensure_import_checkpoints_table($pdo);
$stmtCheckpoint = $pdo->prepare("SELECT last_processed_row FROM drive_test_import_checkpoints WHERE store_key = ? LIMIT 1");
$stmtCheckpoint->execute([$storeKey]);
$checkpointRow = (int) ($stmtCheckpoint->fetchColumn() ?? 0);
$overlapRows = max(0, $overlapRows);
$effectiveStartRow = $checkpointRow > 0
? max($baselineStartRow, $checkpointRow - $overlapRows + 1)
: $baselineStartRow;
$preview = drive_test_fetch_orders(0, $effectiveStartRow, $storeKey);
$driveOrders = $preview["orders"] ?? [];
$driveTotalRows = (int) ($preview["total_rows"] ?? 0);
$sheetLastRow = ($effectiveStartRow - 1) + $driveTotalRows;
$newCheckpointRow = max($checkpointRow, (int) $sheetLastRow);
if (!empty($driveOrders)) {
$stmtUpsert = $pdo->prepare(
"INSERT INTO callcenter_test_orders (
store_key,
source_key,
codigo,
import_id,
drive_imported_at,
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,
: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),
is_agregado = 0,
drive_imported_at = VALUES(drive_imported_at),
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"
);
foreach ($driveOrders as $order) {
$driveImportedAt = drive_test_parse_datetime_mysql($order["import_id"] ?? null);
$stmtUpsert->execute([
":store_key" => $storeKey,
":source_key" => $order["source_key"] ?? "",
":codigo" => ($order["codigo"] ?? "") !== "" ? ($order["codigo"] ?? null) : null,
":import_id" => ($order["import_id"] ?? "") !== "" ? ($order["import_id"] ?? null) : null,
":drive_imported_at" => $driveImportedAt,
":nombre" => ($order["nombre"] ?? "") !== "" ? ($order["nombre"] ?? null) : null,
":direccion_drive" => ($order["direccion"] ?? "") !== "" ? ($order["direccion"] ?? null) : null,
":referencia_drive" => ($order["referencia"] ?? "") !== "" ? ($order["referencia"] ?? null) : null,
":sede_drive" => ($order["sede"] ?? "") !== "" ? ($order["sede"] ?? null) : null,
":ciudad_drive" => ($order["ciudad"] ?? "") !== "" ? ($order["ciudad"] ?? null) : null,
":distrito_drive" => ($order["distrito"] ?? "") !== "" ? ($order["distrito"] ?? null) : null,
":dni_drive" => ($order["dni"] ?? "") !== "" ? ($order["dni"] ?? null) : null,
":observaciones_drive" => ($order["observaciones"] ?? "") !== "" ? ($order["observaciones"] ?? null) : null,
":celular" => ($order["celular"] ?? "") !== "" ? ($order["celular"] ?? null) : null,
":producto" => ($order["producto"] ?? "") !== "" ? ($order["producto"] ?? null) : null,
":cantidad" => ($order["cantidad"] ?? "") !== "" ? ($order["cantidad"] ?? null) : null,
":precio" => ($order["precio"] ?? "") !== "" ? ($order["precio"] ?? null) : null,
":pais" => ($order["pais"] ?? "") !== "" ? ($order["pais"] ?? null) : null,
":coordenadas" => ($order["coordenadas"] ?? "") !== "" ? ($order["coordenadas"] ?? null) : null,
":metodo" => ($order["metodo"] ?? "") !== "" ? ($order["metodo"] ?? null) : null,
]);
}
}
$stmtCheckpointUpsert = $pdo->prepare(
"INSERT INTO drive_test_import_checkpoints (store_key, last_processed_row, last_sync_at)
VALUES (:store_key, :last_processed_row, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
last_processed_row = VALUES(last_processed_row),
last_sync_at = CURRENT_TIMESTAMP"
);
$stmtCheckpointUpsert->execute([
":store_key" => $storeKey,
":last_processed_row" => $newCheckpointRow,
]);
return [
"checkpoint_was" => $checkpointRow,
"last_processed_row" => $newCheckpointRow,
"next_start_row" => $newCheckpointRow + 1,
"effective_start_row" => $effectiveStartRow,
"drive_rows_total" => $driveTotalRows,
"orders_synced_count" => count($driveOrders),
];
}
function drive_test_fetch_orders_from_db(PDO $pdo, string $storeKey, ?bool $onlyAgregados = null): array
{
$storeKey = mb_strtolower(trim($storeKey));
drive_test_ensure_orders_table($pdo);
$where = "WHERE store_key = ?";
$params = [$storeKey];
if ($onlyAgregados === true) {
$where .= " AND is_agregado = 1";
} elseif ($onlyAgregados === false) {
$where .= " AND (is_agregado = 0 OR is_agregado IS NULL)";
}
$stmt = $pdo->prepare(
"SELECT
id,
is_agregado,
source_key,
codigo,
import_id,
nombre,
celular,
producto,
cantidad,
precio,
pais,
coordenadas,
metodo,
direccion_drive,
referencia_drive,
sede_drive,
ciudad_drive,
distrito_drive,
dni_drive,
observaciones_drive
FROM callcenter_test_orders
$where
ORDER BY id DESC"
);
$stmt->execute($params);
$orders = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$orders[] = [
"id" => (int) ($row["id"] ?? 0),
"is_agregado" => (int) ($row["is_agregado"] ?? 0),
"source_key" => (string) ($row["source_key"] ?? ""),
"codigo" => (string) ($row["codigo"] ?? ""),
"import_id" => (string) ($row["import_id"] ?? ""),
"nombre" => (string) ($row["nombre"] ?? ""),
"direccion" => (string) ($row["direccion_drive"] ?? ""),
"referencia" => (string) ($row["referencia_drive"] ?? ""),
"agencia" => "",
"sede_agencia" => "",
"distrito" => (string) ($row["distrito_drive"] ?? ""),
"celular" => (string) ($row["celular"] ?? ""),
"producto" => (string) ($row["producto"] ?? ""),
"cantidad" => (string) ($row["cantidad"] ?? ""),
"precio" => (string) ($row["precio"] ?? ""),
"pais" => (string) ($row["pais"] ?? ""),
"coordenadas" => (string) ($row["coordenadas"] ?? ""),
"ciudad" => (string) ($row["ciudad_drive"] ?? ""),
"metodo" => (string) ($row["metodo"] ?? ""),
"sede" => (string) ($row["sede_drive"] ?? ""),
"dni" => (string) ($row["dni_drive"] ?? ""),
"observaciones" => (string) ($row["observaciones_drive"] ?? ""),
];
}
return $orders;
}