Saltar al contenido
Chatbots de WhatsApp desde cero
Temario Ir al ejercicio →

Panel del dueño con Google Sheets

  • ⏱ 60 minutos
  • 📶 Nivel: intermedio
  • 🧩 Incluye ejercicio

💡 🎯 Al terminar esta lección podrás…

  • Explicar por qué una hoja de cálculo es un buen primer panel para el dueño de un negocio pequeño.
  • Escribir un doPost en Google Apps Script que reciba un pedido, valide un secreto y agregue una fila.
  • Publicar el script como aplicación web y llamarlo desde el Worker con fetch, sin romper el bot si la hoja falla.
  • Convertir un pedido en una fila ordenada y proteger los datos personales de los clientes.
  • Armar una pestaña de panel con totales del día y pedidos por estado.

1.Por qué una hoja de cálculo es un buen panel

El bot ya guarda los pedidos en KV (lección 15), pero KV es un cajón para programadores: el dueño del minimarket no puede abrirlo, filtrarlo ni imprimirlo. Lo que él quiere es ver los pedidos del día, marcarlos como entregados y saber cuánto vendió. Casi todo dueño de un negocio pequeño ya sabe usar una hoja de cálculo, y Google Sheets viene con lo que necesitas: es gratis, se abre desde el celular, se comparte con un clic y permite filtros, colores y fórmulas.

La idea es tratar la hoja como la libreta de pedidos del local. El bot escribe una fila por pedido; el dueño la lee y la edita. No necesitas construir una web de administración, ni programar login, ni pagar una base de datos. Para vender un bot a un negocio pequeño, es una de las mejoras con mejor relación entre esfuerzo y valor.

Hay un obstáculo: el Worker de Cloudflare no puede escribir en una hoja sin un permiso de Google. La solución más simple para empezar es Google Apps Script, un entorno de JavaScript que corre en los servidores de Google, ya está conectado a tu cuenta y puede publicarse como una aplicación web con su propia URL. El Worker le manda un pedido por HTTP; el script lo escribe en la hoja.

Workerbot en Cloudflare
→POST con el pedido (JSON)
Apps Scriptaplicación web
→appendRow
Google Sheetshoja Pedidos
→la abre
Dueñocelular o PC

📌 Nota · Un camino, no el único

Existe también la API oficial de Google Sheets con cuenta de servicio. Es más flexible pero exige configurar un proyecto de Google Cloud y firmar tokens. Apps Script es más corto y suficiente para esta etapa. Cuando el volumen crezca, esa será una mejora posible.

2.Paso 1: preparar la hoja

Crea una hoja de cálculo nueva con una cuenta de Google de pruebas (no la del negocio real de un cliente mientras practicas). Llámala Minimarket La Esquina - Pedidos y renombra la primera pestaña como Pedidos. En la fila 1 escribe los encabezados, en este orden exacto:

A B C D E F G H I
Fecha Pedido Telefono Productos Total Entrega Direccion Pago Estado

Dos ajustes que ahorran problemas:

Revisa también la zona horaria de la hoja en Archivo > Configuración: si no coincide con la del negocio, las horas se verán corridas. (Los nombres exactos de los menús de Google cambian de vez en cuando: si alguno no coincide, busca la opción equivalente.)

3.Paso 2: el script que recibe los pedidos

En la hoja abre Extensiones > Apps Script. Se abrirá un editor con un archivo Código.gs. Reemplaza su contenido por este código. En una aplicación web, una petición POST ejecuta la función doPost(e), y el cuerpo que mandó el Worker llega como texto en e.postData.contents (así lo describe la guía de aplicaciones web).

Código.gs
const HOJA = 'Pedidos';

function responder(obj) {
  return ContentService
    .createTextOutput(JSON.stringify(obj))
    .setMimeType(ContentService.MimeType.JSON);
}

function doPost(e) {
  let datos;
  try {
    datos = JSON.parse(e.postData.contents);
  } catch (err) {
    return responder({ ok: false, error: 'JSON invalido' });
  }

  const esperado = PropertiesService.getScriptProperties().getProperty('HOJA_SECRETO');
  if (!esperado || datos.secreto !== esperado) {
    return responder({ ok: false, error: 'no autorizado' });
  }
  if (datos.accion !== 'nuevo_pedido' || !Array.isArray(datos.fila) || datos.fila.length !== 9) {
    return responder({ ok: false, error: 'peticion invalida' });
  }

  const fila = datos.fila.slice();
  fila[0] = new Date(fila[0]); // texto ISO -> fecha real de la hoja

  const candado = LockService.getScriptLock();
  candado.waitLock(10000); // espera hasta 10 s a que otro pedido termine de escribir
  try {
    const hoja = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(HOJA);
    hoja.appendRow(fila);
  } finally {
    candado.releaseLock();
  }
  return responder({ ok: true });
}

Qué hace cada parte y por qué:

Probar la lógica sin Google

No tienes que esperar a desplegar para saber si la lógica funciona. Ejecuté este mismo código en Node con dobles de prueba (objetos falsos que imitan ContentService, PropertiesService, LockService y SpreadsheetApp) y cuatro llamadas distintas: una correcta, otra con secreto equivocado, otra con acción desconocida y una con texto que no es JSON:

Salida esperada
{"ok":true}
{"ok":false,"error":"no autorizado"}
{"ok":false,"error":"peticion invalida"}
{"ok":false,"error":"JSON invalido"}
1 Pedidos true 2026-10-07T15:00:25.000Z

La última línea confirma que solo se escribió una fila (la correcta), en la pestaña Pedidos, y que la primera celda es un objeto Date real. Lo que este experimento no prueba es la parte de Google (permisos, despliegue): eso se comprueba en el Paso 4.

Guardar el secreto

No pegues el secreto dentro del código. En el editor, abre Configuración del proyecto (el engranaje), baja a Propiedades del script y añade una propiedad HOJA_SECRETO con una frase larga y aleatoria (por ejemplo 32 caracteres que generes tú). El código la lee con PropertiesService.getScriptProperties().getProperty('HOJA_SECRETO'). Guarda ese mismo valor en tu Worker como secreto en el paso siguiente.

4.Paso 3: convertir un pedido en una fila

Ya sabes cómo se ve un pedido que sale del motor. Es el objeto real que produce procesar al confirmar (el id es aleatorio, el tuyo será distinto):

Salida esperada
{
 "id": "LE-Y8IDYG",
 "telefono": "51999000111",
 "items": [{ "id": "leche", "nombre": "Leche entera 1 L", "precio": 4.3, "cantidad": 2 }],
 "total": 8.6,
 "entrega": "recojo",
 "direccion": null,
 "pago": "efectivo",
 "estado": "recibido",
 "creadoEn": 1791385225000
}

(Lo reduje a menos líneas para leerlo mejor; los campos son los mismos.) Necesitamos pasarlo a un arreglo de 9 valores, uno por columna. Creamos un archivo nuevo en tu proyecto, src/hoja.js, sin tocar los archivos de referencia:

src/hoja.js
export function pedidoAFila(p) {
  return [
    new Date(p.creadoEn).toISOString(),
    p.id,
    p.telefono,
    p.items.map((i) => `${i.cantidad} x ${i.nombre}`).join('; '),
    p.total,
    p.entrega,
    p.direccion ?? '',
    p.pago,
    p.estado,
  ];
}

export async function enviarPedidoAHoja(env, pedido, llamar = fetch) {
  const r = await llamar(env.HOJA_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'text/plain;charset=utf-8' },
    body: JSON.stringify({ secreto: env.HOJA_SECRETO, accion: 'nuevo_pedido', fila: pedidoAFila(pedido) }),
  });
  const datos = await r.json().catch(() => ({}));
  if (!datos.ok) throw new Error(`La hoja respondió: ${datos.error ?? r.status}`);
  return datos;
}

Con el pedido de arriba, pedidoAFila entrega (salida real):

Salida esperada
[
  '2026-10-07T15:00:25.000Z',
  'LE-Y8IDYG',
  '51999000111',
  '2 x Leche entera 1 L',
  8.6,
  'recojo',
  '',
  'efectivo',
  'recibido'
]

Detalles de diseño que merecen explicación:

El Worker, a su vez, necesita conocer la URL y el secreto. En wrangler.toml deja un valor de ejemplo y guarda los reales como secretos:

Terminal
npx wrangler secret put HOJA_URL
npx wrangler secret put HOJA_SECRETO

La URL de una aplicación web contiene un identificador largo y permite escribir a tu hoja si alguien sabe el secreto: trátala como un dato privado, no la publiques en tu repositorio.

Conectarlo al bot sin romper nada

Un principio de diseño: la hoja es un extra; si falla, el cliente debe seguir recibiendo su confirmación. Por eso la llamada va en un try/catch que solo registra el error. En procesarMensaje, justo después de guardar el pedido en KV, añadirías:

worker.js
if (sesion.pedidoNuevo) {
  await guardarJson(kv, `pedido:${sesion.pedidoNuevo.id}`, sesion.pedidoNuevo, TTL_PEDIDO);
  await enviarPedidoAHoja(env, sesion.pedidoNuevo, llamar)
    .catch((e) => console.error('No se pudo escribir en la hoja:', e.message));
}

Y no olvides import { enviarPedidoAHoja } from './hoja.js'; al inicio. Esto es un cambio que tú haces en tu copia del proyecto: el archivo de referencia del curso no lo incluye.

Probé el lado Worker uniendo todo con un fetch falso que llama directamente al doPost de arriba. Con el secreto correcto escribe una fila; con un secreto incorrecto el Worker se queda con el error y la hoja no recibe nada:

Salida esperada
{ ok: true }
1
La hoja respondió: no autorizado
1

Las dos últimas líneas son el número de filas escritas antes y después del intento no autorizado: sigue siendo 1.

¿Y si la hoja está caída?

El pedido ya está a salvo en KV (30 días), pero no llegó a la hoja. Una mejora sencilla es guardar una marca de «pendiente de hoja» y reintentarlo en el cron de la lección 18. No la implementamos aquí para mantener la lección enfocada, pero que el dato siga en KV es justamente lo que te permite reintentar.

5.Paso 4: publicar la aplicación web

En el editor de Apps Script:

  1. Pulsa Implementar > Nueva implementación.
  2. Junto a «Seleccionar tipo», elige Aplicación web.
  3. Configura Ejecutar como: Yo (el script corre con tus permisos aunque quien llame sea el Worker) y Quién tiene acceso: Cualquier persona.
  4. Pulsa Implementar, concede los permisos que Google solicite (acceso a tus hojas) y copia la URL que termina en /exec.

La guía oficial explica estas dos opciones: con «Ejecutar la aplicación como yo» el script siempre corre como el propietario; con «como el usuario que accede» corre con la identidad de quien visita. Para un Worker sin cuenta de Google necesitas la primera.

⚠️ Importante · Verifica este dato

Los nombres de menús y opciones de Google cambian con el tiempo (en este texto los traduje del inglés habitual). Si no los encuentras, sigue la guía oficial de aplicaciones web. Dos reglas que sí son estables: la implementación de prueba (URL terminada en /dev) solo funciona para quien edita el script y ejecuta siempre el código guardado más reciente; la URL /exec ejecuta la versión implementada, así que después de cambiar el código debes publicar una nueva versión de la implementación para que el cambio tenga efecto.

Probar con curl

Terminal
curl -L -X POST "https://script.google.com/macros/s/EJEMPLO/exec" \
  -H "Content-Type: text/plain;charset=utf-8" \
  -d '{"secreto":"clave-larga-de-ejemplo","accion":"nuevo_pedido","fila":["2026-10-07T15:00:25.000Z","LE-PRUEBA","51999000111","1 x Pan de molde",6.5,"recojo","","efectivo","recibido"]}'

La opción -L hace que curl siga redirecciones. La razón: según lo que se observa habitualmente, las aplicaciones web de Apps Script responden a un POST con una redirección (código 302) hacia otra dirección de Google donde está el resultado. Esa redirección no está descrita en la página oficial que consulté, así que verifica este dato con tu propio despliegue: si ves Moved Temporarily o un HTML en lugar de {"ok":true}, falta el -L. El fetch de un Worker sigue redirecciones por defecto, por lo que enviarPedidoAHoja no necesita nada especial. Lo importante: el script se ejecuta (y la fila se escribe) en la primera petición; la redirección solo sirve para entregarte la respuesta.

Cambia el secreto por uno incorrecto y deberías ver {"ok":false,"error":"no autorizado"}. Si en cambio recibes una página de inicio de sesión de Google, el acceso no está en «Cualquier persona».

6.Seguridad: la URL es pública, el secreto no

Cuando eliges «Cualquier persona», cualquiera que conozca la URL puede llamarla. No puedes limitar por dirección IP, y el evento e de doPost no incluye las cabeceras HTTP, así que no puedes validar una firma como la del webhook de Meta (verifica este dato en la guía oficial si Google lo cambia). Tu defensa es el secreto compartido dentro del cuerpo. Aplica estas reglas:

7.Paso 5: el panel del dueño

Una hoja con filas es un registro; un panel es lo que el dueño mira primero: «¿cuánto vendí hoy y qué falta entregar?». Crea una segunda pestaña llamada Panel. Las fórmulas siguientes usan la sintaxis en inglés y comas; en una hoja configurada en español, Google traduce los nombres de las funciones (por ejemplo COUNTIFS se muestra como CONTAR.SI.CONJUNTO) y puede pedir punto y coma como separador. No las ejecuté aquí; pruébalas con tus datos y, si Sheets marca error, usa el autocompletado de la propia hoja.

Celda Qué muestra Fórmula
A1 Día a consultar una fecha, por ejemplo 07/10/2026
B2 Pedidos del día =COUNTIFS(Pedidos!A:A,">="&A1,Pedidos!A:A,"<"&A1+1,Pedidos!I:I,"<>cancelado")
B3 Ventas del día (S/) =SUMIFS(Pedidos!E:E,Pedidos!A:A,">="&A1,Pedidos!A:A,"<"&A1+1,Pedidos!I:I,"<>cancelado")
B4 Pedidos por entregar =COUNTIF(Pedidos!I:I,"recibido")

Esto replica la lógica del ejercicio: contar y sumar las filas del día que no estén canceladas. Para que el dueño cambie el estado sin escribir:

Un punto importante de diseño: este panel es de una vía. El bot escribe en la hoja, pero si el dueño cambia el estado a «entregado», el bot no se entera (el pedido en KV sigue como recibido, y por eso podría salir un recordatorio de un pedido que ya se entregó). Cerrar ese ciclo es posible, por ejemplo con un trigger que avise al Worker al editar la hoja, pero añade complejidad. Para la primera versión, avisa al dueño de esa limitación y evita enviar recordatorios de pedidos que ya están marcados como entregados en el negocio real.

🧪 Ejemplo · Una mañana con el panel

Dueño: abre la hoja en su celular a las 8:00 a. m. y ve en Panel 3 pedidos del día por S/ 41.10. Filtra por recibido, prepara las bolsas, y al entregar cada una cambia la celda de Estado a entregado. Al cierre, el total del día ya está calculado: no hace cuentas a mano.

8.Cuotas y límites

Apps Script tiene cuotas por día. Para cuentas personales (como las de gmail.com), la página de cuotas indica, entre otras, 6 minutos de ejecución por llamada y hasta 30 ejecuciones simultáneas por usuario. Para escribir una fila por pedido no es un problema; si tu bot recibiera cientos de pedidos por minuto, tendrías que cambiar de enfoque. Verifica este dato en esa página, porque las cuotas se actualizan y difieren entre cuentas gratuitas y Workspace.

9.Errores frecuentes

10.Apuntes para llevar

11.Glosario

Término Significado
Apps Script Entorno de JavaScript de Google que automatiza Sheets, Docs y otros servicios.
Aplicación web Script publicado con una URL que responde a peticiones HTTP (doGet, doPost).
doPost(e) Función que se ejecuta cuando la aplicación web recibe un POST.
Secreto compartido Clave que solo conocen quien llama y quien recibe, para validar la petición.
appendRow Método que añade una fila al final de los datos de una hoja.
LockService Servicio que evita que dos ejecuciones escriban a la vez.
Propiedades del script Lugar para guardar valores como el secreto, fuera del código.
Panel Pestaña con totales y estados que el dueño consulta de un vistazo.

🧠 Comprueba lo aprendido

Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.

¿Por qué se usa Apps Script en vez de escribir directo a Sheets desde el Worker?

¿Qué función ejecuta una aplicación web de Apps Script cuando recibe un POST?

La aplicación web está abierta a «Cualquier persona». ¿Qué protege tu hoja de pedidos falsos?

El Worker recibe HTTP 200 de la hoja pero la fila no aparece. ¿Qué debes revisar primero?

Si Google Sheets falla justo al confirmar un pedido, ¿cuál es el comportamiento correcto del bot?

El dueño cambia el estado de un pedido a «entregado» en la hoja. ¿Qué pasa con el pedido en KV en este diseño?

📚 Para profundizar (fuentes oficiales)

Este material lo hace un estudiante con ayuda de IA: ante cualquier duda, manda la documentación oficial.

Tu progreso se guarda en este navegador y se refleja en el índice del curso.

💡 🚀 ¡Ahora te toca!

La práctica fija lo aprendido. Pasa al ejercicio de esta lección.

Ir al ejercicio