diff --git a/services/NormalBot.php b/services/NormalBot.php index cfcc9b5..66cd291 100644 --- a/services/NormalBot.php +++ b/services/NormalBot.php @@ -1244,6 +1244,31 @@ class NormalBot ConversationContext::updateMetadata($ctxId, $meta); return self::capAskNext($cap, $context['from'], $company, $ctxId); } + + // Escrito a mano: "1a, 2b 5c" marca varios de una. Un toque por lote + // redibujaba la lista en un mensaje nuevo cada vez, y para seis lotes + // eran seis mensajes. Suma a lo ya marcado, no lo reemplaza. + // Un toque manda siempre un id del catalogo y ya se resolvio arriba: + // lo que llega hasta aca fue escrito a mano. + if (trim($value) !== '') { + [$marcados, $sinReconocer] = self::multiPorTexto($value, $catalogo, $elegidos); + + if ($marcados || $sinReconocer) { + $cap['__multi'][$key] = $marcados; + $meta['__cap'] = $cap; + ConversationContext::updateMetadata($ctxId, $meta); + + // Lo que no reconoce se dice: en silencio se registraria de menos + if ($sinReconocer) { + WhatsAppSender::sendText( + $context['from'], + '⚠️ No encontré: ' . implode(', ', array_map(fn($t) => '"' . $t . '"', $sinReconocer)), + (string)($context['phone_number_id'] ?? '') + ); + } + return self::capAskNext($cap, $context['from'], $company, $ctxId); + } + } } // ── 1b. "Ver más": avanza la página y vuelve a mostrar la misma lista ─ @@ -1626,6 +1651,62 @@ class NormalBot * final anterior a la inicial), que el ERP acepta y recién detecta el * revisor. Las fechas van en YYYY-MM-DD, así que comparar como texto basta. */ + /** + * Interpreta "1a, 2b 5c" contra el catalogo de un multi_select. + * + * Compara sin acentos ni mayusculas y acepta el nombre del lote solo o con + * la finca delante, porque nadie escribe "REPOSO 1A" estando ya en REPOSO. + * Devuelve [elegidos, lo que no reconocio]. + */ + private static function multiPorTexto(string $texto, array $catalogo, array $elegidos): array + { + $normalizar = function (string $t): string { + $t = mb_strtolower(trim($t), 'UTF-8'); + $t = strtr($t, ['á'=>'a','é'=>'e','í'=>'i','ó'=>'o','ú'=>'u','ñ'=>'n']); + return preg_replace('/[^a-z0-9]/', '', $t) ?? ''; + }; + + // Indice del catalogo: nombre completo y ultima palabra ("REPOSO 1A" -> "1a") + $indice = []; + foreach ($catalogo as $id => $etiqueta) { + $limpia = trim(preg_replace('/\s*\(.*?\)\s*$/', '', (string)$etiqueta)); + $indice[$normalizar($limpia)] = (string)$id; + $partes = preg_split('/\s+/', $limpia); + if (count($partes) > 1) { + $corto = $normalizar(end($partes)); + // Solo si no choca con otro lote: si dos fincas tienen "1A" se exige el nombre completo + if ($corto !== '' && !array_key_exists($corto, $indice)) $indice[$corto] = (string)$id; + elseif ($corto !== '' && ($indice[$corto] ?? null) !== (string)$id) $indice[$corto] = null; + } + } + + // Se separa por comas y se prueba el segmento entero antes de partirlo + // por espacios: si no, "RETIRO 1A" se rompia en dos y no matcheaba nada. + $sinReconocer = []; + foreach (preg_split('/[,;\n]+/', $texto, -1, PREG_SPLIT_NO_EMPTY) as $segmento) { + $n = $normalizar($segmento); + if ($n === '') continue; + + if (($indice[$n] ?? null) !== null) { + $id = $indice[$n]; + $elegidos[$id] = $catalogo[$id]; + continue; + } + + // Sin coincidencia completa: pueden ser varios sueltos ("1a 2b 5c") + $partes = preg_split('/\s+/', trim($segmento), -1, PREG_SPLIT_NO_EMPTY); + if (count($partes) < 2) { $sinReconocer[] = trim($segmento); continue; } + foreach ($partes as $token) { + $t = $normalizar($token); + if ($t === '') continue; + $id = $indice[$t] ?? null; + if ($id === null) { $sinReconocer[] = trim($token); continue; } + $elegidos[$id] = $catalogo[$id]; + } + } + return [$elegidos, $sinReconocer]; + } + private static function capValidar(array $field, string $value, array $collected): ?string { $min = $field['min_field'] ?? ''; diff --git a/setup/tests/test_flujos.php b/setup/tests/test_flujos.php index a087232..c5bfe38 100644 --- a/setup/tests/test_flujos.php +++ b/setup/tests/test_flujos.php @@ -146,6 +146,22 @@ check('y la página 2 alcanza el lote 12', isset(filasDe($r)['12'])); $r = NormalBot::processInteractive($co, $ctx, '__mas'); // vuelve: no hay página 3 $r = NormalBot::processInteractive($co, $ctx, '__mas'); +// Escribir varios de una: seis toques eran seis mensajes +$r = NormalBot::process($co, $ctx, '3a, 4a 5a'); +$filas = filasDe($r); +check('escribir marca varios en un solo mensaje', + str_contains($filas['__listo'] ?? '', '(5)')); +// El contador no depende de la pagina que se este viendo +check('suma a lo tocado en vez de reemplazarlo: 2 + 3 = 5', + str_contains($filas['__listo'] ?? '', '(5)')); + +WhatsAppSender::$enviados = []; +$r = NormalBot::process($co, $ctx, '6a, 9z9z'); +check('lo que no reconoce lo avisa, no lo ignora', + str_contains(WhatsAppSender::$enviados[0]['text'] ?? '', '"9z9z"')); +check('y marca igual lo que sí entendió', + str_contains(filasDe($r)['__listo'] ?? '', '(6)')); + $r = NormalBot::processInteractive($co, $ctx, '__listo'); check('resumen final', isset(filasDe($r)['__cap_confirm'])); @@ -153,7 +169,8 @@ capturas(); NormalBot::processInteractive($co, $ctx, '__cap_confirm'); $post = array_values(array_filter(capturas(), fn($c) => str_contains($c['peticion'] ?? '', 'ciclos_')))[0] ?? null; check('la URL resolvió el tipo elegido', $post['peticion'] ?? null, 'ciclos_cosecha_up'); -check('lote_ids como arreglo', $post['body']['lote_ids'] ?? null, ['1', '2']); +check('lote_ids lleva los tocados y los escritos', + $post['body']['lote_ids'] ?? null, ['1', '2', '3', '4', '5', '6']); check('con la acción y la fecha', [$post['body']['accion'] ?? null, $post['body']['fecha'] ?? null], ['apertura', $hoy]);