feat: collect_and_post flow type — sequential field collection + POST

New flow type that asks the user for a configured list of fields one
by one, shows a summary with confirm/cancel, then POSTs all answers
as JSON to the configured endpoint with Bearer auth.

Config example:
  {
    "type": "collect_and_post",
    "endpoint_key": "subir_cosecha",
    "header": "🌿 Ciclos Cosecha",
    "confirm": true,
    "success_text": " Ciclo registrado.",
    "fields": [
      {"key":"fecha",    "label":"Fecha",    "prompt":"📅 ¿Fecha del ciclo? (YYYY-MM-DD)"},
      {"key":"cantidad", "label":"Racimos",  "prompt":"🔢 ¿Cantidad de racimos?"},
      {"key":"finca",    "label":"Finca",    "prompt":"🏡 ¿Nombre de la finca?"}
    ]
  }

POST payload: {fecha, cantidad, finca, telefono, nombre}

Also adds the UI panel in bot-config with dynamic add/remove field rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-29 11:06:42 -05:00
co-authored by Claude Sonnet 4.6
parent 32f87ef27c
commit 3c7ea9fdf9
3 changed files with 268 additions and 0 deletions
+79
View File
@@ -2921,6 +2921,32 @@ HTML;
$showDynList = $ft === 'dynamic_list' ? 'display:flex;flex-direction:column;gap:8px' : 'display:none';
$showSubmit = $ft === 'submit_form' ? 'display:flex;flex-direction:column;gap:8px' : 'display:none';
$showForEach = $ft === 'collect_for_each' ? 'display:flex;flex-direction:column;gap:8px' : 'display:none';
$showCap = $ft === 'collect_and_post' ? 'display:flex;flex-direction:column;gap:8px' : 'display:none';
// collect_and_post fields
$capFields = $flow['fields'] ?? [];
$capHeader = self::h($flow['header'] ?? '');
$capSucc = self::h($flow['success_text'] ?? '');
$capConfirm = !empty($flow['confirm']);
$capEpKey = self::h($flow['endpoint_key'] ?? '');
$capEpSelHtml = '<option value="">— Selecciona endpoint —</option>';
foreach ($companyEndpoints as $ep) {
$sel = $capEpKey === self::h($ep['endpoint_key']) ? ' selected' : '';
$capEpSelHtml .= '<option value="' . self::h($ep['endpoint_key']) . '"' . $sel . '>' . $epLabel($ep) . '</option>';
}
$capFieldsHtml = '';
foreach ($capFields as $cf) {
$ck = self::h($cf['key'] ?? '');
$cl = self::h($cf['label'] ?? '');
$cp = self::h($cf['prompt'] ?? '');
$capFieldsHtml .= "
<div class=\"cap-field-row\" style=\"display:grid;grid-template-columns:1fr 1fr 2fr auto;gap:6px;align-items:center\">
<input type=\"text\" name=\"cap_field_key[]\" value=\"{$ck}\" placeholder=\"clave (ej: fecha)\" style=\"font-family:monospace\">
<input type=\"text\" name=\"cap_field_label[]\" value=\"{$cl}\" placeholder=\"etiqueta\">
<input type=\"text\" name=\"cap_field_prompt[]\" value=\"{$cp}\" placeholder=\"Pregunta al usuario\">
<button type=\"button\" onclick=\"this.closest('.cap-field-row').remove()\" style=\"background:#fee2e2;border:none;border-radius:4px;padding:4px 8px;cursor:pointer\">✕</button>
</div>";
}
$feEpSelHtml = '<option value="">— Selecciona endpoint lista —</option>';
foreach ($companyEndpoints as $ep) {
@@ -2968,6 +2994,7 @@ HTML;
<option value=\"collect_input\"" . ($ft==='collect_input' ?' selected':'') . ">✏️ Pedir dato</option>
<option value=\"dynamic_list\"" . ($ft==='dynamic_list' ?' selected':'') . ">📋 Lista dinámica</option>
<option value=\"collect_for_each\"" . ($ft==='collect_for_each'?' selected':'') . ">🌧 Registro por ítem</option>
<option value=\"collect_and_post\"" . ($ft==='collect_and_post'?' selected':'') . ">📝 Pedir campos y enviar</option>
<option value=\"submit_form\"" . ($ft==='submit_form' ?' selected':'') . ">🚀 Enviar formulario</option>
</optgroup>
</select>
@@ -3053,6 +3080,26 @@ HTML;
<div><label>Mensaje de éxito</label><input type=\"text\" name=\"flow_fe_success[]\" value=\"{$feSuccessText}\" placeholder=\"✅ Datos registrados correctamente.\"></div>
</div>
<!-- collect_and_post -->
<div class=\"ftg-cap\" style=\"{$showCap}\">
<div style=\"display:grid;grid-template-columns:1fr 1fr;gap:8px\">
<div><label>Endpoint (POST)</label><select name=\"flow_cap_ep[]\">{$capEpSelHtml}</select></div>
<div><label>Encabezado</label><input type=\"text\" name=\"flow_cap_header[]\" value=\"{$capHeader}\" placeholder=\"📋 Registro ciclos\"></div>
</div>
<div style=\"display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center\">
<div><label>Mensaje de éxito</label><input type=\"text\" name=\"flow_cap_success[]\" value=\"{$capSucc}\" placeholder=\"✅ Registrado.\"></div>
<label style=\"display:flex;align-items:center;gap:6px;font-size:12px;margin-top:18px\">
<input type=\"checkbox\" name=\"flow_cap_confirm_{$flowIdx}\" value=\"1\"" . ($capConfirm?' checked':'') . "> Pedir confirmación
</label>
</div>
<input type=\"hidden\" name=\"flow_cap_confirm_idx[]\" value=\"{$flowIdx}\">
<div>
<label style=\"margin-bottom:6px;display:block\">Campos a pedir <span style=\"font-size:10px;color:#9ca3af\">(clave · etiqueta · pregunta)</span></label>
<div class=\"cap-fields-container\" id=\"cap-fields-{$flowIdx}\">{$capFieldsHtml}</div>
<button type=\"button\" onclick=\"addCapField('cap-fields-{$flowIdx}')\" style=\"margin-top:6px;font-size:12px;background:#f0f4ff;border:1px solid #c7d7ff;border-radius:4px;padding:4px 10px;cursor:pointer\">+ Campo</button>
</div>
</div>
</div>
</div>
</div>";
@@ -3316,6 +3363,7 @@ function flowTabChange(sel) {
'ftg-dynlist': {show: v==='dynamic_list', flex: true},
'ftg-submit': {show: v==='submit_form', flex: true},
'ftg-foreach': {show: v==='collect_for_each', flex: true},
'ftg-cap': {show: v==='collect_and_post', flex: true},
};
for (const [cls, cfg] of Object.entries(sections)) {
const el = card.querySelector('.' + cls);
@@ -3367,6 +3415,7 @@ function addFlowCard() {
' <option value="collect_input">✏️ Pedir dato</option>' +
' <option value="dynamic_list">📋 Lista dinámica</option>' +
' <option value="collect_for_each">🌧 Registro por ítem</option>' +
' <option value="collect_and_post">📝 Pedir campos y enviar</option>' +
' <option value="submit_form">🚀 Enviar formulario</option>' +
' </optgroup>' +
' </select>' +
@@ -3436,6 +3485,22 @@ function addFlowCard() {
' <div><label>Encabezado</label><input type="text" name="flow_fe_header[]" placeholder="🌧 Pluviometría"></div>' +
' <div><label>Pregunta por ítem</label><input type="text" name="flow_fe_question[]" placeholder="¿Cuál es el valor? (mm)"></div>' +
' <div><label>Mensaje de éxito</label><input type="text" name="flow_fe_success[]" placeholder="✅ Datos registrados."></div>' +
' <div class="ftg-cap" style="display:none;flex-direction:column;gap:8px">' +
' <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">' +
' <div><label>Endpoint (POST)</label><select name="flow_cap_ep[]">' + epOpts + '</select></div>' +
' <div><label>Encabezado</label><input type="text" name="flow_cap_header[]" placeholder="📋 Registro ciclos"></div>' +
' </div>' +
' <div style="display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center">' +
' <div><label>Mensaje de éxito</label><input type="text" name="flow_cap_success[]" placeholder="✅ Registrado."></div>' +
' <label style="display:flex;align-items:center;gap:6px;font-size:12px;margin-top:18px"><input type="checkbox" name="flow_cap_confirm_new" value="1" checked> Confirmar</label>' +
' </div>' +
' <input type="hidden" name="flow_cap_confirm_idx[]" value="new">' +
' <div>' +
' <label style="margin-bottom:6px;display:block">Campos <span style="font-size:10px;color:#9ca3af">(clave · etiqueta · pregunta)</span></label>' +
' <div class="cap-fields-container" id="cap-fields-new-' + cardId + '"></div>' +
' <button type="button" onclick="addCapField(\'cap-fields-new-' + cardId + '\')" style="margin-top:6px;font-size:12px;background:#f0f4ff;border:1px solid #c7d7ff;border-radius:4px;padding:4px 10px;cursor:pointer">+ Campo</button>' +
' </div>' +
' </div>' +
' </div>' +
' </div>' +
' </div>' +
@@ -3444,6 +3509,20 @@ function addFlowCard() {
container.lastElementChild.scrollIntoView({behavior:'smooth'});
}
function addCapField(containerId) {
const c = document.getElementById(containerId);
if (!c) return;
const row = document.createElement('div');
row.className = 'cap-field-row';
row.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 2fr auto;gap:6px;align-items:center;margin-bottom:4px';
row.innerHTML =
'<input type="text" name="cap_field_key[]" placeholder="clave" style="font-family:monospace">' +
'<input type="text" name="cap_field_label[]" placeholder="etiqueta">' +
'<input type="text" name="cap_field_prompt[]" placeholder="Pregunta al usuario">' +
'<button type="button" onclick="this.closest(\'.cap-field-row\').remove()" style="background:#fee2e2;border:none;border-radius:4px;padding:4px 8px;cursor:pointer">✕</button>';
c.appendChild(row);
}
function aiProviderChange(v) {
const oai = document.getElementById('ai-openai-fields');
const gem = document.getElementById('ai-gemini-fields');