feat: flujo de registro de ausentismo (Fase 1, carga de datos)
Primer flujo de POST. El bot recolecta trabajador, motivo y rango de fechas, y postea a ausentismos_up; Palmas360 decide si auto-aprueba o encola para revision, como ya hace bot_entrada. El campo lookup asumia que el endpoint devolvia un solo objeto, pero /empleados devuelve una lista paginada: con eso el trabajador salia como "Desconocido" y sin id. Ahora distingue lista de objeto, y si hay varias coincidencias las ofrece para elegir en vez de quedarse con la primera. Campos opcionales del contrato (eps, diagnostico_id) quedan fuera hasta confirmar con Palmas360 que novedades son incapacidad. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
0bb1d3447e
commit
0e72aa3d5a
+55
-3
@@ -898,8 +898,23 @@ class NormalBot
|
||||
return self::sendText("*{$cap['header']}*\n\n{$prompt}", $context['from'], $company);
|
||||
}
|
||||
|
||||
// ── 3a. Ya se mostraron varias coincidencias: el usuario eligió una ──
|
||||
if (!empty($cap['__lookup_options']) && isset($cap['__lookup_options'][$value])) {
|
||||
$cap['collected'][$field['key']] = $value;
|
||||
$cap['collected_labels'][$field['key']] = $cap['__lookup_options'][$value];
|
||||
unset($cap['__lookup_options']);
|
||||
$cap['index'] = $idx + 1;
|
||||
while ($cap['index'] < count($fields) && self::capShouldSkip($fields[$cap['index']], $cap['collected'])) { $cap['index']++; }
|
||||
$meta['__cap'] = $cap;
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
return $cap['index'] < count($fields)
|
||||
? self::capAskNext($cap, $context['from'], $company, $ctxId)
|
||||
: self::capShowConfirmOrSubmit($cap, $meta, $context, $company, $ctxId);
|
||||
}
|
||||
|
||||
// ── 3. Lookup field — search entity by entered value (e.g. cédula) ───
|
||||
if (($field['type'] ?? 'text') === 'lookup') {
|
||||
unset($cap['__lookup_options']);
|
||||
$result = self::callLookupEndpoint(
|
||||
$field['lookup_endpoint_key'] ?? '',
|
||||
$value,
|
||||
@@ -907,13 +922,38 @@ class NormalBot
|
||||
$company
|
||||
);
|
||||
|
||||
if ($result === null) {
|
||||
$vf = $field['value_field'] ?? 'id';
|
||||
$df = $field['display_field'] ?? 'nombre';
|
||||
|
||||
// Catálogos como /empleados devuelven una lista; /diagnostico_buscar, un objeto.
|
||||
$esLista = is_array($result) && array_is_list($result);
|
||||
if ($esLista) {
|
||||
$result = self::acotarCoincidencias($result, $vf, $df);
|
||||
if (count($result) > 1) {
|
||||
$opciones = [];
|
||||
foreach ($result as $r) $opciones[(string)($r[$vf] ?? '')] = (string)($r[$df] ?? '');
|
||||
$cap['__lookup_options'] = $opciones;
|
||||
$meta['__cap'] = $cap;
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
return self::buildDynamicListResponse($result, [
|
||||
'value_field' => $vf,
|
||||
'label_field' => $df,
|
||||
'header' => $cap['header'],
|
||||
'body' => 'Encontré varias coincidencias. ¿Cuál es?',
|
||||
'button' => 'Ver coincidencias',
|
||||
'section_title' => $field['label'] ?? 'Resultados',
|
||||
], $context['from'], $company);
|
||||
}
|
||||
$result = $result[0] ?? null;
|
||||
}
|
||||
|
||||
if (!$result) {
|
||||
$notFound = $field['not_found_text'] ?? '⚠️ No encontré resultados. Intenta de nuevo:';
|
||||
return self::sendText("*{$cap['header']}*\n\n{$notFound}", $context['from'], $company);
|
||||
}
|
||||
|
||||
$displayName = (string)($result[$field['display_field'] ?? 'nombre'] ?? 'Desconocido');
|
||||
$entityId = (string)($result[$field['value_field'] ?? 'id'] ?? '');
|
||||
$displayName = (string)($result[$df] ?? 'Desconocido');
|
||||
$entityId = (string)($result[$vf] ?? '');
|
||||
|
||||
$cap['__lookup_pending'] = ['id' => $entityId, 'label' => $displayName];
|
||||
$meta['__cap'] = $cap;
|
||||
@@ -971,6 +1011,18 @@ class NormalBot
|
||||
return self::submitCapPost($meta, $context, $company, $ctxId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Una lista de WhatsApp admite pocas filas, y con 40 homónimos elegir no es
|
||||
* viable. Se recortan a 10 y se descartan los que no traigan id o etiqueta.
|
||||
*/
|
||||
private static function acotarCoincidencias(array $items, string $valueField, string $labelField): array
|
||||
{
|
||||
$validos = array_values(array_filter($items, fn($r) =>
|
||||
is_array($r) && ($r[$valueField] ?? '') !== '' && ($r[$labelField] ?? '') !== ''
|
||||
));
|
||||
return array_slice($validos, 0, 10);
|
||||
}
|
||||
|
||||
private static function callLookupEndpoint(string $epKey, string $value, string $param, array $company): ?array
|
||||
{
|
||||
if ($epKey === '' || $value === '') return null;
|
||||
|
||||
+62
-2
@@ -228,7 +228,8 @@ $configJson = [
|
||||
[
|
||||
'title' => 'Opciones',
|
||||
'rows' => [
|
||||
['id' => 'subir_ciclo_cosecha', 'title' => '🌾 Ciclo cosecha', 'description' => 'Subir ciclo de cosecha'],
|
||||
['id' => 'subir_ciclo_cosecha', 'title' => '🌾 Ciclo cosecha', 'description' => 'Subir ciclo de cosecha'],
|
||||
['id' => 'registrar_ausentismo', 'title' => '👥 Ausentismo', 'description' => 'Registrar falta, incapacidad o permiso'],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -395,6 +396,58 @@ $configJson = [
|
||||
'params'=>['endpoint_key'=>'mantenimiento_todos_bot_dn','date_mode'=>'today','filename'=>'mantenimiento_todos.pdf','caption'=>'Mantenimiento — todos los lotes'],
|
||||
],
|
||||
|
||||
// ── Carga de datos (POST) — Fase 1 ──────────────────────────────────
|
||||
// Palmas360 decide si auto-aprueba o encola para revisión; el bot solo
|
||||
// envía. Campos opcionales del contrato (eps, diagnostico_id) quedan
|
||||
// fuera hasta confirmar qué novedades son incapacidad.
|
||||
'registrar_ausentismo' => [
|
||||
'type' => 'collect_and_post',
|
||||
'endpoint_key' => 'ausentismos_up',
|
||||
'header' => '👥 Registrar ausentismo',
|
||||
'confirm' => true,
|
||||
'success_text' => '✅ Ausentismo enviado a Palmas360.',
|
||||
'back' => 'enviar_informacion',
|
||||
'nlu_description' => 'Registrar un ausentismo, incapacidad o permiso de un trabajador',
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'empleado_id',
|
||||
'label' => 'Trabajador',
|
||||
'type' => 'lookup',
|
||||
'prompt' => '👤 Escribe el nombre o documento del trabajador:',
|
||||
'lookup_endpoint_key' => 'empleados_buscar_dn',
|
||||
'lookup_param' => 'filtro',
|
||||
'value_field' => 'id',
|
||||
'display_field' => 'nombre',
|
||||
'not_found_text' => '⚠️ No encontré ese trabajador. Escribe el nombre de nuevo:',
|
||||
],
|
||||
[
|
||||
'key' => 'novedad_id',
|
||||
'label' => 'Motivo',
|
||||
'type' => 'select',
|
||||
'prompt' => '¿Cuál es el motivo del ausentismo?',
|
||||
'source_endpoint_key' => 'novedades_ausentismo_dn',
|
||||
'value_field' => 'id',
|
||||
'label_field' => 'nombre',
|
||||
],
|
||||
[
|
||||
'key' => 'fecha_inicial',
|
||||
'label' => 'Desde',
|
||||
'type' => 'date_quick',
|
||||
'prompt' => '📅 ¿Desde qué fecha?',
|
||||
'other_validate' => 'date',
|
||||
'other_prompt' => 'Escribe la fecha de inicio (YYYY-MM-DD):',
|
||||
],
|
||||
[
|
||||
'key' => 'fecha_final',
|
||||
'label' => 'Hasta',
|
||||
'type' => 'date_quick',
|
||||
'prompt' => '📅 ¿Hasta qué fecha? (si es un solo día, elige la misma)',
|
||||
'other_validate' => 'date',
|
||||
'other_prompt' => 'Escribe la fecha final (YYYY-MM-DD):',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// Upload
|
||||
'enviar_informacion' => ['type' => 'menu', 'menu' => 'submenu_subir_ciclos', 'nlu_skip' => true, 'back' => 'show_main_menu'],
|
||||
'subir_ciclo_cosecha' => ['type' => 'function', 'function' => 'upload_ciclo', 'nlu_description' => 'Subir o enviar un ciclo de cosecha', 'params' => ['ciclo' => 'cosecha']],
|
||||
@@ -425,7 +478,8 @@ $configJson = [
|
||||
[
|
||||
'title' => 'Opciones',
|
||||
'rows' => [
|
||||
['id' => 'subir_ciclo_cosecha', 'title' => '🌾 Subir cosecha', 'description' => 'Enviar ciclo de cosecha'],
|
||||
['id' => 'subir_ciclo_cosecha', 'title' => '🌾 Subir cosecha', 'description' => 'Enviar ciclo de cosecha'],
|
||||
['id' => 'registrar_ausentismo', 'title' => '👥 Ausentismo', 'description' => 'Registrar falta, incapacidad o permiso'],
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -512,6 +566,12 @@ $endpoints = [
|
||||
['key' => 'tratamiento_historico_bot_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=tratamiento_historico_bot&finca_id={finca_id}'],
|
||||
['key' => 'fincas_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=fincas'],
|
||||
['key' => 'numeros_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=whatsapp_numeros&estado=1'],
|
||||
|
||||
// ── Carga de datos (POST) — Fase 1: Ausentismo ──────────────────────────
|
||||
// El lookup agrega &filtro=<texto> a esta URL; estado=1 restringe a activos.
|
||||
['key' => 'empleados_buscar_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=empleados&estado=1&items=10'],
|
||||
['key' => 'novedades_ausentismo_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=novedades_ausentismo_dn'],
|
||||
['key' => 'ausentismos_up', 'dir' => 'upload', 'url' => $BASE . '?peticion=ausentismos_up'],
|
||||
// Todos los lotes (PDF) por ciclo
|
||||
['key' => 'cosecha_todos_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=ciclos_cosecha_dn&finca_id={finca_id}'],
|
||||
['key' => 'polinizacion_todos_dn', 'dir' => 'download', 'url' => $BASE . '?peticion=ciclos_polinizacion_dn&finca_id={finca_id}'],
|
||||
|
||||
@@ -253,6 +253,49 @@ $m2 = [];
|
||||
check('sin nombre no rompe el saludo',
|
||||
str_starts_with($saludar($config, '2', $m2, '2026-08-01', ''), 'Hola,'), true);
|
||||
|
||||
echo "\nAusentismo (POST)\n";
|
||||
|
||||
$aus = $flows['registrar_ausentismo'] ?? [];
|
||||
$campos = array_column($aus['fields'] ?? [], null, 'key');
|
||||
|
||||
// Contrato de BotEntradaProcesador::ausentismo()
|
||||
$requeridos = ['empleado_id', 'novedad_id', 'fecha_inicial', 'fecha_final'];
|
||||
check('recolecta los 4 campos que exige el ERP',
|
||||
array_values(array_intersect($requeridos, array_keys($campos))), $requeridos);
|
||||
|
||||
check('el trabajador se busca por nombre',
|
||||
($campos['empleado_id']['lookup_param'] ?? null), 'filtro');
|
||||
check('lee id/nombre como los devuelve /empleados',
|
||||
[$campos['empleado_id']['value_field'] ?? null, $campos['empleado_id']['display_field'] ?? null],
|
||||
['id', 'nombre']);
|
||||
check('el motivo sale del catálogo del ERP',
|
||||
($campos['novedad_id']['source_endpoint_key'] ?? null), 'novedades_ausentismo_dn');
|
||||
check('ambas fechas validan formato',
|
||||
[$campos['fecha_inicial']['other_validate'] ?? null, $campos['fecha_final']['other_validate'] ?? null],
|
||||
['date', 'date']);
|
||||
check('pide confirmación antes de enviar', $aus['confirm'] ?? false, true);
|
||||
|
||||
// Los endpoints que usa deben estar dados de alta
|
||||
preg_match_all("/'key' => '([^']+)'/", substr($seed, $end), $mEp);
|
||||
$eps = array_flip($mEp[1]);
|
||||
foreach (['ausentismos_up', 'empleados_buscar_dn', 'novedades_ausentismo_dn'] as $k) {
|
||||
check("endpoint {$k} registrado", isset($eps[$k]), true);
|
||||
}
|
||||
|
||||
// Accesible para quienes reportan (cat 1 y cat 3), no para cat 2
|
||||
$enMenu = function (array $config, string $cat, string $id): bool {
|
||||
$pt = $config['per_type'][$cat] ?? [];
|
||||
$menus = array_merge($config['menus'] ?? [], $pt['menus'] ?? []);
|
||||
foreach ($menus as $m) {
|
||||
foreach ($m['sections'] ?? [] as $sec) {
|
||||
if (in_array($id, array_column($sec['rows'] ?? [], 'id'), true)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
check('cat 1 lo ve en su menú', $enMenu($config, '1', 'registrar_ausentismo'), true);
|
||||
check('cat 3 lo ve en enviar información', $enMenu($config, '3', 'registrar_ausentismo'), true);
|
||||
|
||||
echo "\nask_finca\n";
|
||||
$af = $flows['ask_finca'];
|
||||
check('no repregunta si ya hay finca', $af['skip_if_set'] ?? false, true);
|
||||
|
||||
Reference in New Issue
Block a user