feat: perfil por numero — trabajador vinculado y modulos permitidos

Dos perfiles, sin agregar ningun campo para distinguirlos:

- Con tercero vinculado en el ERP, el numero es de un trabajador: from_phone
  pre-llena empleado_id y el flujo se saltea el buscador. Sale gratis porque
  empleados y terceros son la misma tabla, asi que el id sirve tal cual.
- Sin vinculo es supervisor y elige de quien es el registro.

Los modulos habilitados (ciclos, ausentismos, produccion, pluviometria) llegan
del ERP y filtran las filas de menu. Las filas sin modulo declarado no se tocan,
asi la navegacion queda intacta.

Se mantiene la convencion de las fincas: nada marcado significa todo. Los
numeros que ya existen siguen igual sin migrar nada.

Requiere en la base del bot:
  ALTER TABLE company_phones
    ADD COLUMN tercero_id INT NULL, ADD COLUMN modulos_json TEXT NULL;
El codigo tolera que no existan todavia.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-11 20:21:32 -05:00
co-authored by Claude Sonnet 4.6
parent 94d4dcd26a
commit 724179a3c6
3 changed files with 103 additions and 14 deletions
+78
View File
@@ -448,6 +448,67 @@ class NormalBot
}
}
/**
* Perfil sincronizado del numero: con quien esta vinculado y que modulos
* tiene habilitados. Tolera que las columnas aun no existan.
*/
private static function perfilDelNumero(array $company, string $waNumber): array
{
static $cache = [];
$clave = $company['id'] . '|' . $waNumber;
if (isset($cache[$clave])) return $cache[$clave];
$vacio = ['tercero_id' => null, 'modulos' => []];
if ($waNumber === '') return $vacio;
try {
$stmt = db()->prepare(
"SELECT tercero_id, modulos_json FROM company_phones WHERE company_id = ? AND wa_number = ? LIMIT 1"
);
$stmt->execute([(int)$company['id'], $waNumber]);
$row = $stmt->fetch();
} catch (\PDOException $e) {
return $vacio; // columnas aun sin crear
}
if (!$row) return $vacio;
return $cache[$clave] = [
'tercero_id' => $row['tercero_id'] ?: null,
'modulos' => json_decode((string)($row['modulos_json'] ?? ''), true) ?: [],
];
}
/**
* Quita del menu las filas de modulos que este numero no tiene habilitados.
* Sin modulos declarados no se filtra nada: la ausencia significa "todos",
* misma convencion que las fincas.
*/
private static function filtrarMenuPorModulos(array $menu, array $company, string $waNumber): array
{
$perfil = self::perfilDelNumero($company, $waNumber);
if (!$perfil['modulos']) return $menu;
$permitidos = array_merge(
(array)($perfil['modulos']['descarga'] ?? []),
(array)($perfil['modulos']['carga'] ?? [])
);
foreach ($menu['sections'] ?? [] as $i => $sec) {
$filas = array_values(array_filter($sec['rows'] ?? [], fn($r) =>
!isset($r['modulo']) || in_array($r['modulo'], $permitidos, true)
));
$menu['sections'][$i]['rows'] = $filas;
}
$menu['sections'] = array_values(array_filter($menu['sections'] ?? [], fn($s) => !empty($s['rows'])));
foreach ($menu['buttons'] ?? [] as $i => $btn) {
if (isset($btn['modulo']) && !in_array($btn['modulo'], $permitidos, true)) unset($menu['buttons'][$i]);
}
if (isset($menu['buttons'])) $menu['buttons'] = array_values($menu['buttons']);
return $menu;
}
/**
* Guarda la opción elegida con su etiqueta, para poder mostrarle al usuario
* en qué finca/grupo está parado sin tener que volver a consultar la API.
@@ -997,6 +1058,18 @@ class NormalBot
$collected = [];
$startIdx = 0;
// from_phone: el dato sale del perfil del numero, no se pregunta. Con un
// tercero vinculado el trabajador reporta lo suyo y el bot no le pide
// que se busque a si mismo; sin vinculo es supervisor y elige.
$perfil = self::perfilDelNumero($company, $context['from'] ?? '');
foreach ($fields as $f) {
$origen = $f['from_phone'] ?? '';
if ($origen !== '' && !empty($perfil[$origen])) {
$collected[$f['key']] = (string)$perfil[$origen];
}
}
if (!empty($entities)) {
foreach ($fields as $i => $field) {
$fKey = $field['key'] ?? '';
@@ -1015,6 +1088,10 @@ class NormalBot
}
}
while ($startIdx < count($fields) && isset($collected[$fields[$startIdx]['key'] ?? ''])) {
$startIdx++;
}
$meta['__cap'] = [
'fields' => $fields,
'index' => $startIdx,
@@ -2097,6 +2174,7 @@ class NormalBot
private static function buildMenuResponse(array $menu, string $to, array $company, ?int $ctxId = null): ?array
{
$menu = self::filtrarMenuPorModulos($menu, $company, $to);
$menu = self::aplicarVarsMenu($menu, $ctxId, $company);
$menuType = $menu['type'] ?? 'list';
+11 -2
View File
@@ -71,11 +71,13 @@ class PhoneSync
$db = db();
$upsert = $db->prepare("
INSERT INTO company_phones (company_id, wa_number, label, permission_type, is_active)
VALUES (?, ?, ?, ?, 1)
INSERT INTO company_phones (company_id, wa_number, label, permission_type, tercero_id, modulos_json, is_active)
VALUES (?, ?, ?, ?, ?, ?, 1)
ON DUPLICATE KEY UPDATE
label = VALUES(label),
permission_type = VALUES(permission_type),
tercero_id = VALUES(tercero_id),
modulos_json = VALUES(modulos_json),
is_active = 1
");
@@ -84,11 +86,18 @@ class PhoneSync
$waNumber = trim($n['wa_number'] ?? '');
if ($waNumber === '') continue;
// tercero_id vinculado = perfil trabajador: el bot no le pregunta
// de quién es el registro. Sin él, es supervisor y elige.
$tercero = !empty($n['tercero_id']) ? (int)$n['tercero_id'] : null;
$modulos = (array)($n['modulos'] ?? []);
$upsert->execute([
$companyId,
$waNumber,
trim($n['nombre'] ?? ''),
(int)($n['permiso'] ?? 1),
$tercero,
$modulos ? json_encode($modulos) : null,
]);
$activeNumbers[] = $waNumber;
}
+14 -12
View File
@@ -97,9 +97,9 @@ $configJson = [
[
'title' => 'Informes',
'rows' => [
['id' => 'submenu_produccion', 'title' => '🌿 Producción', 'description' => 'Informes de producción'],
['id' => 'submenu_ciclos', 'title' => '🔄 Ciclos', 'description' => 'Ciclos de campo'],
['id' => 'submenu_ausentismos', 'title' => '👥 Ausentismos', 'description' => 'Faltas y novedades'],
['id' => 'submenu_produccion', 'modulo' => 'produccion', 'title' => '🌿 Producción', 'description' => 'Informes de producción'],
['id' => 'submenu_ciclos', 'modulo' => 'ciclos', 'title' => '🔄 Ciclos', 'description' => 'Ciclos de campo'],
['id' => 'submenu_ausentismos', 'modulo' => 'ausentismos', 'title' => '👥 Ausentismos', 'description' => 'Faltas y novedades'],
],
],
],
@@ -230,9 +230,9 @@ $configJson = [
[
'title' => 'Opciones',
'rows' => [
['id' => 'registrar_ausentismo', 'title' => '👥 Ausentismo', 'description' => 'Registrar falta, incapacidad o permiso'],
['id' => 'registrar_pluviometria','title' => '🌧️ Pluviometría', 'description' => 'Milímetros por finca · pronto formulario'],
['id' => 'registrar_ciclo', 'title' => '🔄 Ciclos', 'description' => 'Abrir o cerrar lotes · pronto formulario'],
['id' => 'registrar_ausentismo', 'modulo' => 'ausentismos', 'title' => '👥 Ausentismo', 'description' => 'Registrar falta, incapacidad o permiso'],
['id' => 'registrar_pluviometria','modulo' => 'pluviometria', 'title' => '🌧️ Pluviometría', 'description' => 'Milímetros por finca · pronto formulario'],
['id' => 'registrar_ciclo', 'modulo' => 'ciclos', 'title' => '🔄 Ciclos', 'description' => 'Abrir o cerrar lotes · pronto formulario'],
],
],
],
@@ -416,6 +416,8 @@ $configJson = [
'key' => 'empleado_id',
'label' => 'Trabajador',
'type' => 'lookup',
// Numero vinculado a un trabajador: no se le pregunta
'from_phone' => 'tercero_id',
'prompt' => '👤 Escribe el nombre o documento del trabajador:',
'lookup_endpoint_key' => 'empleados_buscar_dn',
'lookup_param' => 'filtro',
@@ -560,9 +562,9 @@ $configJson = [
[
'title' => 'Opciones',
'rows' => [
['id' => 'registrar_ausentismo', 'title' => '👥 Ausentismo', 'description' => 'Registrar falta, incapacidad o permiso'],
['id' => 'registrar_pluviometria','title' => '🌧️ Pluviometría', 'description' => 'Milímetros por finca · pronto formulario'],
['id' => 'registrar_ciclo', 'title' => '🔄 Ciclos', 'description' => 'Abrir o cerrar lotes · pronto formulario'],
['id' => 'registrar_ausentismo', 'modulo' => 'ausentismos', 'title' => '👥 Ausentismo', 'description' => 'Registrar falta, incapacidad o permiso'],
['id' => 'registrar_pluviometria','modulo' => 'pluviometria', 'title' => '🌧️ Pluviometría', 'description' => 'Milímetros por finca · pronto formulario'],
['id' => 'registrar_ciclo', 'modulo' => 'ciclos', 'title' => '🔄 Ciclos', 'description' => 'Abrir o cerrar lotes · pronto formulario'],
],
],
],
@@ -592,9 +594,9 @@ $configJson = [
[
'title' => 'Informes',
'rows' => [
['id' => 'submenu_produccion', 'title' => '🌿 Producción', 'description' => 'Informes de producción'],
['id' => 'submenu_ciclos', 'title' => '🔄 Ciclos', 'description' => 'Ciclos de campo'],
['id' => 'submenu_ausentismos', 'title' => '👥 Ausentismos', 'description' => 'Faltas y novedades'],
['id' => 'submenu_produccion', 'modulo' => 'produccion', 'title' => '🌿 Producción', 'description' => 'Informes de producción'],
['id' => 'submenu_ciclos', 'modulo' => 'ciclos', 'title' => '🔄 Ciclos', 'description' => 'Ciclos de campo'],
['id' => 'submenu_ausentismos', 'modulo' => 'ausentismos', 'title' => '👥 Ausentismos', 'description' => 'Faltas y novedades'],
],
],
],