oscarviedma

Código AppScript Registro de Evento

Apr 3rd, 2026
282
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.84 KB | None | 0 0
  1. // ===============================================================
  2. // REGISTRO DE EVENTO — Google Apps Script
  3. // ===============================================================
  4.  
  5. const HOJAS = {
  6. REGISTROS: 'Registros',
  7. CONFIGURACION: 'Configuracion',
  8. CAMPOS: 'Campos',
  9. };
  10.  
  11.  
  12. // ===============================================================
  13. // doGet — enruta todas las peticiones
  14. // ===============================================================
  15. function doGet(e) {
  16. try {
  17. const accion = e.parameter.action;
  18.  
  19. if (accion === 'campos') return devolverCampos();
  20. if (accion === 'cupos') return devolverCupos();
  21. if (accion === 'registrar') return registrarParticipante(e.parameter);
  22.  
  23. return respuesta({ status: 'ok', mensaje: 'API activa.' });
  24.  
  25. } catch (err) {
  26. return respuesta({ status: 'error', mensaje: 'Error interno: ' + err.message });
  27. }
  28. }
  29.  
  30.  
  31. // ===============================================================
  32. // LEER CONFIGURACION (uso interno — solo para correos)
  33. // ===============================================================
  34. function leerConfig() {
  35. const hoja = obtenerOCrearHojaConfig();
  36. const datos = hoja.getDataRange().getValues();
  37. const config = {};
  38.  
  39. for (let i = 1; i < datos.length; i++) {
  40. const clave = datos[i][0] ? datos[i][0].toString().trim() : '';
  41. const valor = datos[i][1] !== undefined ? datos[i][1].toString().trim() : '';
  42. if (clave) config[clave] = valor;
  43. }
  44.  
  45. return config;
  46. }
  47.  
  48.  
  49. // ===============================================================
  50. // LEER CAMPOS desde hoja "Campos"
  51. // ===============================================================
  52. function leerCampos() {
  53. const hoja = obtenerOCrearHojaCampos();
  54. const datos = hoja.getDataRange().getValues();
  55. const campos = [];
  56.  
  57. if (datos.length < 2) return campos;
  58.  
  59. for (let i = 1; i < datos.length; i++) {
  60. const fila = datos[i];
  61. const nombre = fila[0] ? fila[0].toString().trim() : '';
  62. if (!nombre) continue;
  63.  
  64. campos.push({
  65. nombre: nombre,
  66. etiqueta: fila[1] ? fila[1].toString().trim() : nombre,
  67. tipo: fila[2] ? fila[2].toString().trim().toLowerCase() : 'text',
  68. placeholder: fila[3] ? fila[3].toString().trim() : '',
  69. requerido: fila[4] ? fila[4].toString().trim().toLowerCase() === 'true' : false,
  70. opciones: fila[5] ? fila[5].toString().split(',').map(o => o.trim()).filter(Boolean) : [],
  71. orden: fila[6] ? parseInt(fila[6]) : i,
  72. });
  73. }
  74.  
  75. campos.sort((a, b) => a.orden - b.orden);
  76. return campos;
  77. }
  78.  
  79. function devolverCampos() {
  80. return respuesta({ campos: leerCampos() });
  81. }
  82.  
  83.  
  84. // ===============================================================
  85. // CUPOS DISPONIBLES
  86. // ===============================================================
  87. function devolverCupos() {
  88. const config = leerConfig();
  89. const cupoMaximo = config.CUPO_MAXIMO ? parseInt(config.CUPO_MAXIMO) : 0;
  90. const hoja = obtenerOCrearHojaRegistros([]);
  91. const registrados = Math.max(0, hoja.getLastRow() - 1);
  92.  
  93. return respuesta({
  94. registrados: registrados,
  95. maximo: cupoMaximo,
  96. ilimitado: cupoMaximo === 0,
  97. });
  98. }
  99.  
  100.  
  101. // ===============================================================
  102. // SINCRONIZAR COLUMNAS
  103. // Detecta campos nuevos en hoja Campos y los agrega a Registros
  104. // siempre antes de las columnas Estado y Espera #
  105. // ===============================================================
  106. function sincronizarColumnas(hoja, campos) {
  107. const encabezadosActuales = hoja.getRange(1, 1, 1, hoja.getLastColumn()).getValues()[0];
  108.  
  109. // Columnas fijas que siempre van al final
  110. const COLS_FIJAS = ['Estado', 'Espera #'];
  111.  
  112. // Detectar qué etiquetas de campos NO están aun en la hoja
  113. const etiquetasActuales = encabezadosActuales.filter(h => h !== '');
  114. const etiquetasFaltantes = campos
  115. .map(c => c.etiqueta)
  116. .filter(etiqueta => !etiquetasActuales.includes(etiqueta));
  117.  
  118. if (etiquetasFaltantes.length === 0) return; // nada que hacer
  119.  
  120. // Insertar cada columna nueva antes de "Estado"
  121. const colEstado = encabezadosActuales.indexOf('Estado') + 1; // base 1
  122.  
  123. etiquetasFaltantes.forEach((etiqueta, i) => {
  124. // Insertar columna antes de Estado (que se va desplazando)
  125. hoja.insertColumnBefore(colEstado + i);
  126. const colNueva = colEstado + i;
  127. // Escribir encabezado con mismo formato que los existentes
  128. const celdaHeader = hoja.getRange(1, colNueva);
  129. celdaHeader.setValue(etiqueta);
  130. celdaHeader.setBackground('#0f0e0d').setFontColor('#ffffff').setFontWeight('bold');
  131. // Rellenar filas existentes con guion
  132. const ultFila = hoja.getLastRow();
  133. if (ultFila > 1) {
  134. hoja.getRange(2, colNueva, ultFila - 1, 1).setValue('—');
  135. }
  136. Logger.log('Columna nueva agregada: ' + etiqueta);
  137. });
  138. }
  139.  
  140.  
  141. // ===============================================================
  142. // REGISTRAR PARTICIPANTE
  143. // ===============================================================
  144. function registrarParticipante(datos) {
  145. const config = leerConfig();
  146. const campos = leerCampos();
  147. const hoja = obtenerOCrearHojaRegistros(campos);
  148.  
  149. // Sincronizar columnas antes de escribir
  150. // Si hay campos nuevos en Sheets, se agregan automaticamente
  151. sincronizarColumnas(hoja, campos);
  152.  
  153. const cupoMaximo = config.CUPO_MAXIMO ? parseInt(config.CUPO_MAXIMO) : 0;
  154. const registrados = Math.max(0, hoja.getLastRow() - 1);
  155.  
  156. // 1. Validar campos requeridos
  157. for (const campo of campos) {
  158. if (campo.requerido && !datos[campo.nombre]) {
  159. return respuesta({ status: 'error', mensaje: 'El campo "' + campo.etiqueta + '" es requerido.' });
  160. }
  161. }
  162.  
  163. // 2. Validar email
  164. const campEmail = campos.find(c => c.tipo === 'email');
  165. if (campEmail) {
  166. const emailVal = datos[campEmail.nombre] || '';
  167. if (!esEmailValido(emailVal)) {
  168. return respuesta({ status: 'error', mensaje: 'El correo no es valido.' });
  169. }
  170. if (emailYaRegistrado(hoja, emailVal, campos)) {
  171. return respuesta({ status: 'error', mensaje: 'Este correo ya esta registrado.' });
  172. }
  173. }
  174.  
  175. // 3. Verificar cupo
  176. const sinLimite = cupoMaximo === 0;
  177. const estado = sinLimite || registrados < cupoMaximo ? 'Confirmado' : 'Lista de espera';
  178.  
  179. let posicionEspera = 0;
  180. if (estado === 'Lista de espera') {
  181. posicionEspera = contarEspera(hoja) + 1;
  182. }
  183.  
  184. // 4. Armar fila leyendo encabezados reales del Sheet
  185. // Esto respeta el orden exacto de columnas incluyendo las nuevas
  186. const encabezados = hoja.getRange(1, 1, 1, hoja.getLastColumn()).getValues()[0];
  187. const timestamp = new Date().toLocaleString('es-MX', { timeZone: 'America/Mexico_City' });
  188.  
  189. const filaDatos = encabezados.map(header => {
  190. if (header === 'Timestamp') return timestamp;
  191. if (header === 'Estado') return estado;
  192. if (header === 'Espera #') return posicionEspera > 0 ? '#' + posicionEspera : '—';
  193. const campo = campos.find(c => c.etiqueta === header);
  194. return campo ? (datos[campo.nombre] || '—') : '—';
  195. });
  196.  
  197. hoja.appendRow(filaDatos);
  198.  
  199. // Colorear celda de estado
  200. const filaEscrita = hoja.getLastRow();
  201. const colEstado = encabezados.indexOf('Estado') + 1;
  202. if (colEstado > 0) {
  203. if (estado === 'Confirmado') {
  204. hoja.getRange(filaEscrita, colEstado).setBackground('#e8f5e9').setFontColor('#2e7d32');
  205. } else {
  206. hoja.getRange(filaEscrita, colEstado).setBackground('#fff8e1').setFontColor('#f57f17');
  207. }
  208. }
  209.  
  210. // 5. Enviar correos
  211. const enviarEmail = config.ENVIAR_EMAIL !== 'false';
  212. const notifOrganizador = config.NOTIF_ORGANIZADOR !== 'false';
  213.  
  214. if (enviarEmail && campEmail) {
  215. enviarConfirmacion(datos, campos, config, estado, posicionEspera);
  216. }
  217.  
  218. if (notifOrganizador && config.EMAIL_ORGANIZADOR) {
  219. notificarOrganizador(datos, campos, config, estado, registrados + 1, cupoMaximo);
  220. }
  221.  
  222. // 6. Responder
  223. if (estado === 'Confirmado') {
  224. return respuesta({ status: 'ok', mensaje: 'Registro confirmado.' });
  225. } else {
  226. return respuesta({ status: 'espera', posicion: posicionEspera, mensaje: 'Cupo completo. Agregado a lista de espera.' });
  227. }
  228. }
  229.  
  230.  
  231. // ===============================================================
  232. // EMAIL — Confirmacion al participante
  233. // ===============================================================
  234. function enviarConfirmacion(datos, campos, config, estado, posicionEspera) {
  235. try {
  236. const campEmail = campos.find(c => c.tipo === 'email');
  237. if (!campEmail) return;
  238.  
  239. const emailDestino = datos[campEmail.nombre];
  240. const campNombre = campos.find(c => c.nombre === 'nombre');
  241. const nombreVal = campNombre ? (datos[campNombre.nombre] || '') : '';
  242.  
  243. let asunto, cuerpo;
  244.  
  245. if (estado === 'Confirmado') {
  246. asunto = 'Tu registro esta confirmado — ' + (config.NOMBRE_EVENTO || 'Evento');
  247.  
  248. let filasDetalle = '';
  249. if (config.FECHA_EVENTO) filasDetalle += '<p style="margin:0 0 8px;"><strong>Fecha:</strong> ' + config.FECHA_EVENTO + '</p>';
  250. if (config.HORA_EVENTO) filasDetalle += '<p style="margin:0 0 8px;"><strong>Horario:</strong> ' + config.HORA_EVENTO + '</p>';
  251. if (config.LINK_ACCESO) filasDetalle += '<p style="margin:0;"><strong>Enlace:</strong> <a href="' + config.LINK_ACCESO + '">' + config.LINK_ACCESO + '</a></p>';
  252.  
  253. cuerpo = '<div style="font-family:Arial,sans-serif;max-width:560px;margin:auto;color:#1a1a1a;">'
  254. + '<h2 style="margin-bottom:4px;">Hola' + (nombreVal ? ', ' + nombreVal : '') + '!</h2>'
  255. + '<p style="color:#555;margin-bottom:24px;">Tu registro al <strong>' + (config.NOMBRE_EVENTO || 'evento') + '</strong> fue confirmado con exito.</p>'
  256. + '<div style="background:#f5f5f5;border-radius:12px;padding:20px 24px;margin-bottom:24px;">' + filasDetalle + '</div>'
  257. + '<p style="color:#777;font-size:13px;">Guarda este correo. El enlace de acceso es solo para uso personal.</p>'
  258. + '</div>';
  259.  
  260. } else {
  261. asunto = 'En lista de espera — ' + (config.NOMBRE_EVENTO || 'Evento');
  262. cuerpo = '<div style="font-family:Arial,sans-serif;max-width:560px;margin:auto;color:#1a1a1a;">'
  263. + '<h2>Hola' + (nombreVal ? ', ' + nombreVal : '') + '</h2>'
  264. + '<p>El evento esta lleno, pero quedaste en la <strong>lista de espera #' + posicionEspera + '</strong>.</p>'
  265. + '<p>Si se libera un lugar, te notificaremos de inmediato.</p>'
  266. + '</div>';
  267. }
  268.  
  269. MailApp.sendEmail({
  270. to: emailDestino,
  271. subject: asunto,
  272. htmlBody: cuerpo,
  273. name: config.NOMBRE_REMITENTE || 'Registro de Evento',
  274. });
  275.  
  276. } catch (err) {
  277. Logger.log('Error email confirmacion: ' + err.message);
  278. }
  279. }
  280.  
  281.  
  282. // ===============================================================
  283. // EMAIL — Notificacion al organizador
  284. // ===============================================================
  285. function notificarOrganizador(datos, campos, config, estado, totalRegistrados, cupoMaximo) {
  286. try {
  287. const asunto = '[Nuevo registro] ' + estado + ' — ' + (config.NOMBRE_EVENTO || 'Evento');
  288.  
  289. let filasDatos = '';
  290. campos.forEach(campo => {
  291. filasDatos += '<p><strong>' + campo.etiqueta + ':</strong> ' + (datos[campo.nombre] || '—') + '</p>';
  292. });
  293.  
  294. const totalStr = cupoMaximo > 0
  295. ? totalRegistrados + ' / ' + cupoMaximo
  296. : totalRegistrados + ' (sin limite)';
  297.  
  298. const cuerpo = '<div style="font-family:Arial,sans-serif;color:#1a1a1a;">'
  299. + '<h3>Nuevo registro recibido</h3>'
  300. + filasDatos
  301. + '<p><strong>Estado:</strong> ' + estado + '</p>'
  302. + '<hr/>'
  303. + '<p style="color:#777;">Total registrados: ' + totalStr + '</p>'
  304. + '</div>';
  305.  
  306. MailApp.sendEmail({
  307. to: config.EMAIL_ORGANIZADOR,
  308. subject: asunto,
  309. htmlBody: cuerpo,
  310. });
  311.  
  312. } catch (err) {
  313. Logger.log('Error email organizador: ' + err.message);
  314. }
  315. }
  316.  
  317.  
  318. // ===============================================================
  319. // CREAR HOJAS
  320. // ===============================================================
  321. function obtenerOCrearHojaRegistros(campos) {
  322. const ss = SpreadsheetApp.getActiveSpreadsheet();
  323. let hoja = ss.getSheetByName(HOJAS.REGISTROS);
  324.  
  325. if (!hoja) {
  326. hoja = ss.insertSheet(HOJAS.REGISTROS);
  327. const headers = ['Timestamp'];
  328. campos.forEach(c => headers.push(c.etiqueta));
  329. headers.push('Estado', 'Espera #');
  330. hoja.getRange(1, 1, 1, headers.length).setValues([headers]);
  331. hoja.getRange(1, 1, 1, headers.length)
  332. .setBackground('#0f0e0d').setFontColor('#ffffff').setFontWeight('bold');
  333. hoja.setFrozenRows(1);
  334. }
  335.  
  336. return hoja;
  337. }
  338.  
  339. function obtenerOCrearHojaConfig() {
  340. const ss = SpreadsheetApp.getActiveSpreadsheet();
  341. let hoja = ss.getSheetByName(HOJAS.CONFIGURACION);
  342.  
  343. if (!hoja) {
  344. hoja = ss.insertSheet(HOJAS.CONFIGURACION);
  345.  
  346. hoja.getRange(1, 1, 1, 3).setValues([['Clave', 'Valor', 'Descripcion']]);
  347. hoja.getRange(1, 1, 1, 3)
  348. .setBackground('#0f0e0d').setFontColor('#ffffff').setFontWeight('bold');
  349. hoja.setFrozenRows(1);
  350.  
  351. const defaults = [
  352. ['CUPO_MAXIMO', '30', 'Numero maximo de registros. Pon 0 para ilimitado.'],
  353. ['ENVIAR_EMAIL', 'true', 'true = enviar confirmacion por correo al participante'],
  354. ['NOTIF_ORGANIZADOR', 'true', 'true = notificar al organizador en cada registro'],
  355. ['EMAIL_ORGANIZADOR', '[email protected]', 'Correo del organizador para notificaciones'],
  356. ['NOMBRE_REMITENTE', 'Registro de Evento', 'Nombre que aparece como remitente en los correos'],
  357. ['NOMBRE_EVENTO', 'Nombre de tu evento', 'Aparece en el asunto y cuerpo del correo'],
  358. ['FECHA_EVENTO', 'Sabado 17 de Mayo, 2025', 'Aparece en el correo de confirmacion'],
  359. ['HORA_EVENTO', '10:00 AM - 2:00 PM', 'Aparece en el correo de confirmacion'],
  360. ['LINK_ACCESO', 'https://zoom.us/j/XXXXXXXXX', 'Enlace Zoom, Meet u otro. Aparece en el correo'],
  361. ];
  362.  
  363. hoja.getRange(2, 1, defaults.length, 3).setValues(defaults);
  364. hoja.setColumnWidth(1, 180);
  365. hoja.setColumnWidth(2, 240);
  366. hoja.setColumnWidth(3, 360);
  367. hoja.getRange(2, 3, defaults.length, 1)
  368. .setFontColor('#888888').setFontStyle('italic');
  369. }
  370.  
  371. return hoja;
  372. }
  373.  
  374. function obtenerOCrearHojaCampos() {
  375. const ss = SpreadsheetApp.getActiveSpreadsheet();
  376. let hoja = ss.getSheetByName(HOJAS.CAMPOS);
  377.  
  378. if (!hoja) {
  379. hoja = ss.insertSheet(HOJAS.CAMPOS);
  380.  
  381. const headers = ['nombre', 'etiqueta', 'tipo', 'placeholder', 'requerido', 'opciones', 'orden'];
  382. hoja.getRange(1, 1, 1, headers.length).setValues([headers]);
  383. hoja.getRange(1, 1, 1, headers.length)
  384. .setBackground('#0f0e0d').setFontColor('#ffffff').setFontWeight('bold');
  385. hoja.setFrozenRows(1);
  386.  
  387. const ejemplos = [
  388. ['nombre', 'Nombre', 'text', 'Tu nombre', 'true', '', '1'],
  389. ['apellido', 'Apellido', 'text', 'Tu apellido', 'true', '', '2'],
  390. ['email', 'Correo electronico', 'email', '[email protected]', 'true', '', '3'],
  391. ['telefono', 'WhatsApp', 'tel', '+52 55 1234 5678', 'false', '', '4'],
  392. ['sesion', 'Sesion de interes', 'select', '', 'true', 'Opcion 1, Opcion 2, Opcion 3', '5'],
  393. ];
  394.  
  395. hoja.getRange(2, 1, ejemplos.length, 7).setValues(ejemplos);
  396. hoja.setColumnWidth(1, 130);
  397. hoja.setColumnWidth(2, 180);
  398. hoja.setColumnWidth(3, 100);
  399. hoja.setColumnWidth(4, 200);
  400. hoja.setColumnWidth(5, 90);
  401. hoja.setColumnWidth(6, 280);
  402. hoja.setColumnWidth(7, 70);
  403.  
  404. hoja.getRange(1, 3).setNote('Tipos validos: text, email, tel, textarea, select');
  405. hoja.getRange(1, 5).setNote('Escribe true o false');
  406. hoja.getRange(1, 6).setNote('Solo para tipo "select". Separa las opciones con coma.');
  407. }
  408.  
  409. return hoja;
  410. }
  411.  
  412.  
  413. // ===============================================================
  414. // INICIALIZAR — ejecutar manualmente una sola vez
  415. // ===============================================================
  416. function ovReInicializarHojas() {
  417. obtenerOCrearHojaConfig();
  418. obtenerOCrearHojaCampos();
  419. obtenerOCrearHojaRegistros([]);
  420. Logger.log('Hojas creadas correctamente. Ya puedes editar Configuracion y Campos.');
  421. }
  422.  
  423.  
  424. // ===============================================================
  425. // HELPERS
  426. // ===============================================================
  427. function emailYaRegistrado(hoja, email, campos) {
  428. const ultFila = hoja.getLastRow();
  429. if (ultFila < 2) return false;
  430.  
  431. const campEmail = campos.find(c => c.tipo === 'email');
  432. if (!campEmail) return false;
  433.  
  434. // Buscar columna por encabezado en lugar de posicion fija
  435. const encabezados = hoja.getRange(1, 1, 1, hoja.getLastColumn()).getValues()[0];
  436. const colEmail = encabezados.indexOf(campEmail.etiqueta) + 1;
  437. if (colEmail < 1) return false;
  438.  
  439. const emails = hoja.getRange(2, colEmail, ultFila - 1, 1).getValues().flat();
  440. return emails.some(e => e.toString().toLowerCase() === email.toLowerCase());
  441. }
  442.  
  443. function contarEspera(hoja) {
  444. const ultFila = hoja.getLastRow();
  445. if (ultFila < 2) return 0;
  446. const encabezados = hoja.getRange(1, 1, 1, hoja.getLastColumn()).getValues()[0];
  447. const colEstado = encabezados.indexOf('Estado') + 1;
  448. if (colEstado < 1) return 0;
  449. const estados = hoja.getRange(2, colEstado, ultFila - 1, 1).getValues().flat();
  450. return estados.filter(s => s === 'Lista de espera').length;
  451. }
  452.  
  453. function esEmailValido(email) {
  454. return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  455. }
  456.  
  457. function respuesta(obj) {
  458. return ContentService
  459. .createTextOutput(JSON.stringify(obj))
  460. .setMimeType(ContentService.MimeType.JSON);
  461. }
Advertisement
Add Comment
Please, Sign In to add comment