feat: marcar varios lotes escribiendolos, no de a uno

Cada toque redibuja la lista en un mensaje nuevo: seis lotes eran seis
mensajes. Ahora se puede escribir "1a, 2b 5c" y quedan marcados de una. Suma a
lo ya tocado en vez de reemplazarlo, asi las dos formas conviven.

Compara sin acentos ni mayusculas y acepta el lote solo o con la finca delante.
Si dos fincas tienen un "1A" no adivina: exige el nombre completo. Y lo que no
reconoce lo dice —marcar cinco de seis en silencio registraria un ciclo de
menos sin que nadie se entere.

Se separa por comas y se prueba el segmento entero antes de partirlo por
espacios: al reves, "RETIRO 1A" se rompia en dos y no matcheaba nada.

Descartado "marcar todos": nunca se abren todos los lotes de una finca.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-20 23:11:47 -05:00
co-authored by Claude Sonnet 4.6
parent 52dbd1e203
commit dd894e5aab
2 changed files with 99 additions and 1 deletions
+81
View File
@@ -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'] ?? '';
+18 -1
View File
@@ -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]);