/** * WOMAKO — webhook instantâneo para a aba "leitura" (planilha WOMAKO). * Página1 (pendências CLIENTE/PEDIDO/PRODUZIDO/PENDÊNCIA) NÃO é tocada. * Implantar a partir da planilha WOMAKO como App da Web: Executar como Eu, * Quem pode acessar: Qualquer pessoa. * * REGRA: cada código de produto ocupa UMA ÚNICA linha. A coluna "quantidade entrada" * guarda o SALDO atual. Use a ação "upsert" (o servidor só usa esta): * { action: "upsert", token, row: { codigo, aliases[], referencia, descricao, saldo, * responsavel, cadastrado, fotoUrl | photoBase64/photoMime/photoName }, remove?: true } * - procura a linha pelo código (coluna A = codigo ou qualquer alias; ou coluna B = * referência quando o produto é cadastrado); * - saldo <= 0 ou remove=true -> apaga todas as linhas desse código; * - senão atualiza a primeira linha encontrada (quantidade = saldo, responsável, foto nova * se enviada, senão mantém a foto anterior) e apaga duplicatas; se não achar, acrescenta. * "append" (legado) agora também faz upsert para nunca duplicar o código. */ var SHARED_TOKEN = 'WMBRSf_xmb4stintvaKfByPev983ulEWToXaEXW8_jA'; var SPREADSHEET_ID = '1oKn1p8QRka3TMTz2TbLgzoaLB21ofU9GXn3DXuJ1OgQ'; // WOMAKO var SHEET_NAME = 'leitura'; var HEADER = [ 'Código de barra', 'Referência', 'Descrição', 'Foto', 'quantidade entrada', 'Responsável' ]; var PHOTO_FOLDER_NAME = 'Pallets fotos'; var PHOTO_FOLDER_ID = '1QOx7GQ8Xakvwq7VAYNHsUkXQ2zCCRJIo'; function doPost(e) { var lock = LockService.getDocumentLock(); lock.waitLock(30000); try { var body = parseBody_(e); if (!body || body.token !== SHARED_TOKEN) { return json_({ ok: false, error: 'unauthorized' }, 401); } var action = String(body.action || '').toLowerCase(); if (action === 'upsert') { return json_(handleUpsert_(body)); } if (action === 'append') { // Legado: converte para upsert (uma linha por código). return json_(handleLegacyAppend_(body)); } if (action === 'delete') { return json_(handleDelete_(body)); } if (action === 'clear') { return json_(handleClear_()); } return json_({ ok: false, error: 'unknown action' }); } catch (err) { return json_({ ok: false, error: String(err && err.message ? err.message : err) }); } finally { try { lock.releaseLock(); } catch (ignore) {} } } function doGet() { return ContentService .createTextOutput(JSON.stringify({ ok: true, service: 'womako-leitura-webhook' })) .setMimeType(ContentService.MimeType.JSON); } function parseBody_(e) { if (!e || !e.postData || !e.postData.contents) return null; try { return JSON.parse(e.postData.contents); } catch (err) { return null; } } function json_(obj) { return ContentService .createTextOutput(JSON.stringify(obj)) .setMimeType(ContentService.MimeType.JSON); } function getSheet_() { // Sempre a planilha WOMAKO (não depende de onde o script está colado). var ss = SpreadsheetApp.openById(SPREADSHEET_ID); var sh = ss.getSheetByName(SHEET_NAME); if (!sh) { sh = ss.insertSheet(SHEET_NAME); } ensureHeader_(sh); return sh; } function ensureHeader_(sh) { var range = sh.getRange(1, 1, 1, HEADER.length); var values = range.getValues()[0]; var empty = true; for (var i = 0; i < HEADER.length; i++) { if (String(values[i] || '').trim() !== '') { empty = false; break; } } if (empty) { range.setValues([HEADER]); } // Padrão Ismael: cabeçalho verde #00FF00, negrito, preto, centralizado. range.setBackground('#00FF00') .setFontWeight('bold') .setFontColor('#000000') .setHorizontalAlignment('center') .setVerticalAlignment('middle'); sh.setFrozenRows(1); } function toNumber_(v) { if (v === undefined || v === null || v === '') return 0; var n = Number(String(v).replace(',', '.')); return isNaN(n) ? 0 : n; } function findRowsForCode_(sh, codes, referencia, matchRef) { var last = sh.getLastRow(); if (last < 2) return []; var values = sh.getRange(2, 1, last - 1, 2).getDisplayValues(); var found = []; for (var i = 0; i < values.length; i++) { var a = String(values[i][0] || '').trim(); var b = String(values[i][1] || '').trim(); if ((a && codes[a]) || (matchRef && referencia && b === referencia)) { found.push(i + 2); } } return found; } function handleUpsert_(body) { var row = body.row || body; var codigo = String(row.codigo || row.codigoBarra || '').trim(); if (!codigo) return { ok: false, error: 'codigo obrigatório' }; var codes = {}; codes[codigo] = true; (row.aliases || []).forEach(function (c) { var t = String(c || '').trim(); if (t) codes[t] = true; }); var referencia = String(row.referencia || '').trim(); var matchRef = row.cadastrado === true; var saldo = toNumber_(row.saldo !== undefined ? row.saldo : row.quantidade); var sh = getSheet_(); var rowsFound = findRowsForCode_(sh, codes, referencia, matchRef); if (body.remove === true || row.remove === true || saldo <= 0) { for (var d = rowsFound.length - 1; d >= 0; d--) sh.deleteRow(rowsFound[d]); return { ok: true, action: 'upsert', codigo: codigo, deleted: rowsFound.length }; } var fotoCell = null; // null = manter a foto atual var driveFileId = ''; var photoB64 = row.photoBase64 || row.fotoBase64 || ''; if (photoB64) { var mime = String(row.photoMime || 'image/jpeg'); var name = String(row.photoName || ('foto-' + Date.now() + '.jpg')); var file = savePhotoToDrive_(photoB64, mime, name); if (file) { driveFileId = file.getId(); fotoCell = '=HYPERLINK("' + file.getUrl() + '";"Ver foto")'; } } else if (row.fotoUrl || row.photoUrl) { fotoCell = '=HYPERLINK("' + String(row.fotoUrl || row.photoUrl) + '";"Ver foto")'; } var values = [ codigo, referencia, String(row.descricao || '').trim(), fotoCell, saldo, String(row.responsavel || '').trim() ]; var target; if (rowsFound.length) { target = rowsFound[0]; if (fotoCell === null) { var cur = sh.getRange(target, 4); values[3] = cur.getFormula() || cur.getValue() || 'sem foto'; } // Código como texto (não vira número/data) sh.getRange(target, 1, 1, 3).setNumberFormat('@'); sh.getRange(target, 1, 1, 6).setValues([values]); for (var k = rowsFound.length - 1; k >= 1; k--) sh.deleteRow(rowsFound[k]); } else { if (fotoCell === null) values[3] = 'sem foto'; sh.appendRow(values); target = sh.getLastRow(); sh.getRange(target, 1, 1, 3).setNumberFormat('@'); sh.getRange(target, 1, 1, 3).setValues([[values[0], values[1], values[2]]]); } return { ok: true, action: 'upsert', codigo: codigo, sheetRow: target, saldo: saldo, driveFileId: driveFileId, driveUrl: driveFileId ? ('https://drive.google.com/file/d/' + driveFileId + '/view') : '' }; } function handleLegacyAppend_(body) { var row = body.row || body; var saldo = row.saldo; if (saldo === undefined || saldo === null || saldo === '') { saldo = row.saldoAtual !== undefined && row.saldoAtual !== '' ? row.saldoAtual : (row.quantidadeEntrada || row.quantidade); } var up = { codigo: row.codigoUnidade || row.codigo || row.codigoBarra, aliases: [row.codigo || ''], referencia: row.referencia, descricao: row.descricao, saldo: (body.saldoZero === true || row.saldoZero === true) ? 0 : saldo, responsavel: row.responsavel, photoBase64: row.photoBase64 || row.fotoBase64 || '', photoMime: row.photoMime, photoName: row.photoName, fotoUrl: row.fotoUrl || row.photoUrl || '' }; return handleUpsert_({ row: up }); } // Antigo append (uma linha por leitura) — mantido só como referência, não é mais usado. function handleAppend_(body) { var row = body.row || body; var codigo = String(row.codigo || row.codigoBarra || '').trim(); var referencia = String(row.referencia || '').trim(); var descricao = String(row.descricao || '').trim(); var q = row.quantidadeEntrada; if (q === undefined || q === null || q === '') q = row.quantidade; var quantidade = q; try { if (quantidade !== '' && quantidade !== null && quantidade !== undefined) { var n = Number(String(quantidade).replace(',', '.')); if (!isNaN(n)) quantidade = (Math.floor(n) === n) ? n : n; } } catch (ignore) {} var responsavel = String(row.responsavel || '').trim(); var fotoCell = 'sem foto'; var photoB64 = row.photoBase64 || row.fotoBase64 || ''; var photoMime = String(row.photoMime || row.fotoMime || 'image/jpeg').trim() || 'image/jpeg'; var photoName = String(row.photoName || row.fotoNome || '').trim(); if (!photoName) { photoName = (row.id ? String(row.id) : ('foto-' + Date.now())) + '.jpg'; } if (photoB64) { var file = savePhotoToDrive_(photoB64, photoMime, photoName); if (file) { fotoCell = '=HYPERLINK("' + file.getUrl() + '";"Ver foto")'; } } else if (row.fotoUrl || row.photoUrl) { fotoCell = '=HYPERLINK("' + String(row.fotoUrl || row.photoUrl) + '";"Ver foto")'; } else if (row.foto && String(row.foto).indexOf('http') === 0) { // fallback: first URL in foto field var first = String(row.foto).split(/\s+/)[0]; if (first.indexOf('http') === 0) { fotoCell = '=HYPERLINK("' + first + '";"Ver foto")'; } } var sh = getSheet_(); sh.appendRow([codigo, referencia, descricao, fotoCell, quantidade, responsavel]); // When stock hits zero, remove all rows for this barcode (parity with sync). if (body.saldoZero === true || row.saldoZero === true) { handleDelete_({ codigos: [codigo] }); } return { ok: true, action: 'append', codigo: codigo }; } function handleDelete_(body) { var sh = getSheet_(); var codigos = {}; var ids = {}; (body.codigos || []).forEach(function (c) { var t = String(c || '').trim(); if (t) codigos[t] = true; }); (body.ids || []).forEach(function (id) { var t = String(id || '').trim(); if (t) ids[t] = true; }); if (body.codigo) { var one = String(body.codigo).trim(); if (one) codigos[one] = true; } if (body.id) { var oneId = String(body.id).trim(); if (oneId) ids[oneId] = true; } var hasFilter = Object.keys(codigos).length > 0 || Object.keys(ids).length > 0; if (!hasFilter) { return { ok: false, error: 'delete requires ids or codigos' }; } var last = sh.getLastRow(); if (last < 2) return { ok: true, deleted: 0 }; // Column A = código. Optional hidden id is not on sheet; match by barcode. var values = sh.getRange(2, 1, last, 1).getValues(); var toDelete = []; for (var i = values.length - 1; i >= 0; i--) { var code = String(values[i][0] || '').trim(); if (code && codigos[code]) { toDelete.push(i + 2); // 1-based sheet row } } for (var j = 0; j < toDelete.length; j++) { sh.deleteRow(toDelete[j]); } return { ok: true, action: 'delete', deleted: toDelete.length }; } function handleClear_() { var sh = getSheet_(); var last = sh.getLastRow(); if (last >= 2) { sh.getRange(2, 1, last, Math.max(sh.getLastColumn(), HEADER.length)).clearContent(); // Prefer deleting empty data rows to keep sheet tidy try { sh.deleteRows(2, last - 1); } catch (ignore) {} } ensureHeader_(sh); return { ok: true, action: 'clear' }; } function savePhotoToDrive_(b64, mime, name) { var raw = String(b64 || ''); // Accept data URL or raw base64 var comma = raw.indexOf(','); if (raw.indexOf('base64') >= 0 && comma >= 0) { raw = raw.substring(comma + 1); } var blob = Utilities.newBlob(Utilities.base64Decode(raw), mime || 'image/jpeg', name || 'foto.jpg'); var folder = getOrCreatePhotoFolder_(); var file = folder.createFile(blob); try { file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); } catch (ignore) {} return file; } function getOrCreatePhotoFolder_() { try { if (PHOTO_FOLDER_ID) return DriveApp.getFolderById(PHOTO_FOLDER_ID); } catch (ignore) {} var ss = SpreadsheetApp.getActiveSpreadsheet(); var parents = DriveApp.getFileById(ss.getId()).getParents(); var parent = parents.hasNext() ? parents.next() : DriveApp.getRootFolder(); var it = parent.getFoldersByName(PHOTO_FOLDER_NAME); if (it.hasNext()) return it.next(); return parent.createFolder(PHOTO_FOLDER_NAME); }