Xây dựng và chạy các chuỗi tác vụ tự động hóa (workflows) tùy chỉnh trong WME. Tự động điều hướng từ file Excel/CSV/GG Sheets và thực thi các hành động được điều chỉnh sẵn bằng WME SDK.
// ==UserScript==
// @name WME Workflow Engine
// @namespace https://greasyfork.org/
// @version 2.1.5
// @description Xây dựng và chạy các chuỗi tác vụ tự động hóa (workflows) tùy chỉnh trong WME. Tự động điều hướng từ file Excel/CSV/GG Sheets và thực thi các hành động được điều chỉnh sẵn bằng WME SDK.
// @author Minh Tan
// @match https://www.waze.com/editor*
// @match https://www.waze.com/*/editor*
// @match https://beta.waze.com/editor*
// @match https://beta.waze.com/*/editor*
// @exclude https://www.waze.com/*user/editor*
// @connect script.google.com
// @connect googleusercontent.com
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @require https://greasyfork.org/scripts/560385/code/WazeToastr.js
// @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// @require https://update.greasyfork.org/scripts/389765/1090053/CommonUtils.js
// @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iNTBtbSIKICAgaGVpZ2h0PSI1MG1tIgogICB2aWV3Qm94PSIwIDAgNTAgNTAiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzEiCiAgIGlua3NjYXBlOnZlcnNpb249IjEuNC4yIChmNDMyN2Y0LCAyMDI1LTA1LTEzKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0id21lLXdmLWUuc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzEiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjMDAwMDAwIgogICAgIGJvcmRlcm9wYWNpdHk9IjAuMjUiCiAgICAgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIwIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iCiAgICAgaW5rc2NhcGU6em9vbT0iMy4wNDY3MTcyIgogICAgIGlua3NjYXBlOmN4PSI4Ni40ODY1MzEiCiAgICAgaW5rc2NhcGU6Y3k9IjEzNi4zNzYzIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMjU2MCIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDU3IgogICAgIGlua3NjYXBlOndpbmRvdy14PSItOCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTgiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiIC8+CiAgPGRlZnMKICAgICBpZD0iZGVmczEiPgogICAgPHBhdHRlcm4KICAgICAgIGlua3NjYXBlOmNvbGxlY3Q9ImFsd2F5cyIKICAgICAgIHhsaW5rOmhyZWY9IiNhcDEwOCIKICAgICAgIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiCiAgICAgICBpZD0icGF0dGVybjE4IgogICAgICAgcGF0dGVyblRyYW5zZm9ybT0ibWF0cml4KDAuMDUsMCwwLDAuMDUsMTE4MC4wMDM0LDU0My4yNjE4NSkiCiAgICAgICB4PSI5NyIKICAgICAgIHk9IjAiIC8+CiAgICA8cGF0dGVybgogICAgICAgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIKICAgICAgIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIgogICAgICAgd2lkdGg9IjM3Ny44NzUzNSIKICAgICAgIGhlaWdodD0iMTk1Ljk0ODkzIgogICAgICAgcGF0dGVyblRyYW5zZm9ybT0idHJhbnNsYXRlKDExODAuMDAzNCw1NDMuMjYxODUpIHNjYWxlKDAuMikiCiAgICAgICBzdHlsZT0iZmlsbDojZjYwMDAwIgogICAgICAgaWQ9ImFwMTA4IgogICAgICAgaW5rc2NhcGU6Y29sbGVjdD0iYWx3YXlzIgogICAgICAgaW5rc2NhcGU6aXNzdG9jaz0idHJ1ZSIKICAgICAgIGlua3NjYXBlOmxhYmVsPSJhcDEwOCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTkzODYiCiAgICAgICAgIHN0eWxlPSJvcGFjaXR5OjE7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjkuMDA0OTE7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1vcGFjaXR5OjE7cGFpbnQtb3JkZXI6bWFya2VycyBmaWxsIHN0cm9rZSIKICAgICAgICAgZD0ibSAwLDAgdiAyLjIxNDA0NzIgYyA0LjQ1OTA0ODgsMCA3Ljk5MTU4NDMsMy41MzI0MjIxIDcuOTkxNTg0Myw3Ljk5MTQ3MDggMCwyLjMzNDgwMyAtMC45NzYxMzg2LDQuNDA2MjQ5IC0yLjUzOTAxMTEsNS44NTQ1MjYgQyAzLjY0ODExMzQsMTUuOTU0NTk1IDEuODMwNTc2NCwxNS45MDA1NDggMCwxNS45MDA1NDggdiA4LjUxNDc0NiBDIDM2LjExNzU4MSwyNC40MTkwNzQgNjYuODMzODc3LDQ3LjI5MTM3NiA3OC41MjcyMDYsNzkuMzA5MzggNTQuMzA2NTU3LDgzLjUwMzMzMiAzMy4yODM3NjcsOTcuMDc1MjM4IDE5LjMwOTY4MiwxMTYuMTM4NTcgMTMuMDgxMTM0LDExNC44MDY3OCA2LjYyMjI2MTQsMTE0LjEwMDggMCwxMTQuMTAwOCB2IDguNTE0NjMgYyAzNi4wNTY3MzEsMCA2Ni43Mjk5MDIsMjIuNzUzOTMgNzguNDY2NTQ1LDU0LjcwNjgxIC0zLjQ3NzMxNywwLjYwNDYxIC02Ljg4NzY5OCwxLjM5OTM3IC0xMC4yMjE5OTcsMi4zNzkzNyBDIDU3LjYwOTI5OCwxNTIuMzg2NTggMzEuMDM4MTYxLDEzMi45ODA3MSAwLDEzMi45ODA3MSB2IDguNTMxMjYgYyAyNy4zOTMxODQsMCA1MC43NTU4OCwxNi45NTk4IDYwLjE5NzkzNCw0MC45NjUzNiAtMy4yNDQ3MjUsMS4yODg3NCAtNi40MDIxMDQsMi43NTI4MiAtOS40NTY1MjksNC4zODQxIEMgNDIuOTMxMzUxLDE2Ni40MzUxNiAyMy4xMzQyOTksMTUxLjg4Mjg5IDAsMTUxLjg4Mjc3IHYgOC41MzEyNiBjIDIwLjExMzA1OCwxLjJlLTQgMzcuMTQyMjQ5LDEyLjg4NjggNDMuMzA2MTI5LDMwLjg3NTQ5IC0yLjIwMTk1MywxLjQ1MzU0IC00LjMxNTk1NiwzLjAyOTgyIC02LjM4MzI0NCw0LjY1OTQxIEggNTIuMTAxNzcgYyAxMi40NTAzNjksLTcuMjI3MjkgMjYuOTE1MzM5LC0xMS4zNzg2MSA0Mi4zNjk4NjUsLTExLjM3ODYxIDE1LjQ0NjU4NSwwIDI5Ljg5NzI3NSw0LjE1NzAyIDQyLjM0MjM0NSwxMS4zNzg2MSBoIDE1LjIyODQ0IGMgLTExLjg2ODU1LC05LjM2ODc3IC0yNi4wNTA1MSwtMTUuOTI5MiAtNDEuNTYwMTgsLTE4LjYyNjY5IDExLjc0OTUsLTMxLjkxNDg2IDQyLjQwOTgyLC01NC43MDEyNSA3OC40NTU0NCwtNTQuNzAxMjUgMzYuMDM4NzcsMCA2Ni42OTUxNiwyMi43Nzk4NSA3OC40NTAwMiw1NC42ODQ3NCAtMTUuNTQ4NDEsMi42ODc1OCAtMjkuNzY3NTIsOS4yNTQyOCAtNDEuNjY0ODcsMTguNjQzMiBoIDE1LjIzNCBjIDEyLjQ0NDU0LC03LjIyMjQ5IDI2Ljg5NTA4LC0xMS4zNzg2MSA0Mi4zNDIzMSwtMTEuMzc4NjEgMTUuNDU3NTUsMCAyOS45MjI1Miw0LjE0ODk3IDQyLjM3NTM0LDExLjM3ODYxIGggMTUuMTY3ODkgYyAtMi4wMzgyMywtMS42MDc3NCAtNC4xMTk2MSwtMy4xNjI0OSAtNi4yODk2NiwtNC41OTg4NiA2LjE0NjcyLC0xOC4wMTk3MyAyMy4xODc1OSwtMzAuOTM1OTIgNDMuMzIyNjQsLTMwLjkzNjA0IHYgLTguNTMxMjYgYyAtMjMuMTUzMjMsMS4yZS00IC00Mi45NjY3MywxNC41NzUzMyAtNTAuNzYzNCwzNS4wMjgyMSAtMy4wNTQyNywtMS42MzU3NCAtNi4yMDU4NywtMy4xMDc3MiAtOS40NTEwMSwtNC40MDA1IDkuNDMzMzIsLTI0LjAyMzg5IDMyLjgwODM0LC00MC45OTg1MSA2MC4yMTQ0MSwtNDAuOTk4NTEgdiAtOC41MzEyNiBjIC0zMS4wNDg0OCwwIC01Ny42Mjc0OCwxOS40MTg4NyAtNjguMjU1NDcsNDYuNzQ4NDEgLTMuMzMyMTEsLTAuOTgzMjEgLTYuNzQwOTQsLTEuNzgyMiAtMTAuMjE2NTksLTIuMzkwMzYgMTEuNzMyMjksLTMxLjk2MTU0IDQyLjQwOTMyLC01NC43MjMzMyA3OC40NzIwNiwtNTQuNzIzMzMgdiAtOC41MTQ2MyBjIC02LjY1NTQxLDAgLTEzLjE0NTgsMC43MTQ5IC0xOS40MDMyMiwyLjA1OTggLTEzLjk1MDAxLC0xOS4wNDg0NzQgLTM0LjkzODY0LC0zMi42MTgwMzYgLTU5LjEyMzkxLC0zNi44MzQ3MDQgMTEuNjkyOTksLTMyLjAxOTcwNCA0Mi40MDkxLC01NC45MDU5OSA3OC41MjcwOSwtNTQuOTEwNjAyIHYgLTguNTE0NzQ2IGMgLTEuODI5ODUsMi4yN2UtNCAtMy42NDg3MSwwLjA1NDA1IC01LjQ1MjQ2LDAuMTU5NDk2IC0xLjU2MjgzLC0xLjQ0ODI3NyAtMi41MzkwMSwtMy41MTk3MjMgLTIuNTM5MDEsLTUuODU0NTI2IDAsLTQuNDU5MDQ4NyAzLjUzMjQ2LC03Ljk5MTQ3MDggNy45OTE0NywtNy45OTE0NzA4IFYgMCBoIC0xMi45MjYyNSBjIC0yLjIzMjExLDIuODE2MjM5NCAtMy41Nzk5Niw2LjM2MDkwNzEgLTMuNTc5OTYsMTAuMjA1NTE4IDAsMi40NjA4ODggMC41NTEyOCw0LjgwNjU3NiAxLjUzMTA4LDYuOTEyIC0xLjUzMzA1LDAuMjUwOTYxIC0zLjA1MzQ4LDAuNTM4ODg1IC00LjU2MDIyLDAuODY0NjggQyAzNTYuMDc1LDE0LjkwNDg2OSAzNTMuNjI2MjQsMTEuOTcxMzg5IDM1MS4wMDkzNCw5LjE5NzcwNzEgMzUxLjEyOTE1LDUuOTQyODE1NyAzNTEuODQ1ODYsMi44NTM5NTkxIDM1My4wMTQxMSwwIGggLTguOTk5NCBjIC0wLjIwMjU4LDAuNjY4NTYwNjMgLTAuNDA5NywxLjMzNjkzMjMgLTAuNTcyNzgsMi4wMjEyNTM1IEMgMzQyLjYxODQxLDEuMzIyNzU5MSAzNDEuNzU2MjIsMC42Njk4NDU2NyAzNDAuOTA4NDMsMCBoIC0xNS4xNjc4NCBjIDkuMDk0MDMsNS4yOTM4MzMxIDE3LjEyMDMxLDEyLjIxOTA5OSAyMy42NDk1MiwyMC40MDAwMzggLTMuODQ5NjQsMS4yNTE5NjggLTcuNTkzLDIuNzQ1MzM1IC0xMS4yMDI0MSw0LjQ3MjIzOSBDIDMyNC45OTI1OCw5Ljg2MTgwNzkgMzA1Ljc2NjM5LDAuMjk2Mjc3MTcgMjg0LjMxODAyLDAgaCAtMi4wMzIyOSBDIDI2MC44NjU5OCwwLjI5NDQyNTIgMjQxLjY1OTA2LDkuODMxNzIyOCAyMjguNDYwMTIsMjQuODA2MTczIDIyNC44NDg5MywyMy4wODU2OTQgMjIxLjEwMjk3LDIxLjU5NTcyOSAyMTcuMjUyMiwyMC4zNTA0ODggMjIzLjc3NDQ5LDEyLjE4OTY1NyAyMzEuNzg2NjMsNS4yODM0MDE2IDI0MC44NjMyMSwwIGggLTE1LjE5NTQ0IGMgLTYuNTU2NjEsNS4xODUxMzM5IC0xMi40MjM0NiwxMS4yMDgwNzYgLTE3LjM4NzQyLDE3Ljk0MzcyMyAtNi4yMzg3OSwtMS4zMzY0NzkgLTEyLjcwODczLC0yLjA0MzMyNiAtMTkuMzQyNjcsLTIuMDQzMzI2IC02LjY2OTAyLDAgLTEzLjE3MjUzLDAuNzE1MDQ5IC0xOS40NDE3MSwyLjA2NTI4NSBDIDE2NC41Mjk4NiwxMS4yMjExOTEgMTU4LjY1OTg0LDUuMTkwODAzMSAxNTIuMDk3NTIsMCBoIC0xNS4xODk5NSBjIDkuMDg2NDMsNS4yODgzMTUgMTcuMTA2MjEsMTIuMjAxMjk4IDIzLjYzMzA4LDIwLjM3MjUyMyAtMy44NDk4MywxLjI0OTAyIC03LjU5MjU0LDIuNzQ4MTMyIC0xMS4yMDI0NSw0LjQ3MjEyNiBDIDEzNi4xNDM2MSw5Ljg0ODY5MjkgMTE2LjkyNzI0LDAuMjk1ODYxNDIgOTUuNDkwNTIsMCBIIDkzLjQ1Mjc1IEMgNzIuMDE5NjU0LDAuMjk1OTM3MDEgNTIuODAzNzgsOS44NDc0MDc5IDM5LjYwNTE0LDI0LjgzOTI0NCAzNS45OTQzNjksMjMuMTE1OTY5IDMyLjI1ODkxLDIxLjYyMDc4NyAyOC40MDgxMzksMjAuMzcyNTIzIDM0LjkzNDI0OSwxMi4yMDE2IDQyLjk1NTY1NCw1LjI4ODQyODMgNTIuMDQxMjIyLDAgSCAzNi44NjIzNzUgQyAzNi4wNDU0MywwLjY0NTk1OTA2IDM1LjIxMTU1MywxLjI3MjExMzQgMzQuNDE2OTA3LDEuOTQ0MTg5IDM0LjI1ODE2NywxLjI4NTAwMTYgMzQuMDU1NTg0LDAuNjQ0MzcxNjUgMzMuODYwNjc0LDAgaCAtOS4wMDQ4NzYgYyAxLjE1NTUxNSwyLjgyMjI4NjYgMS44NzM0MzcsNS44NzIxMzg2IDIuMDA0NzM3LDkuMDg3NDk2MSAtMi42NTE0NTIsMi44MDA0NDA5IC01LjEzMjQwOSw1Ljc2MTI4NDkgLTcuNDI0MjM5LDguODcyNzQyOSAtMS40NzU0OSwtMC4zMTc0OCAtMi45NjU5ODQsLTAuNTk3MDUyIC00LjQ2NjYwOCwtMC44NDI3MjEgMC45ODExNjYsLTIuMTA2NTk1IDEuNTM2NjQzLC00LjQ0OTI5OCAxLjUzNjY0MywtNi45MTIgQyAxNi41MDYzMzEsNi4zNjA5MDcxIDE1LjE1ODQzOCwyLjgxNjIzOTQgMTIuOTI2MzYyLDAgWiBtIDk0LjQ3MTYzNSw4LjU5MTgxMSBjIDE4LjYwOTQxNSwwIDM1LjM1MTU0NSw3Ljg1MzQ0MyA0Ny4xMTE4NDUsMjAuNDI3NjI4IC0zLjU0MTE5LDIuMTI5MzQ4IC02LjkyODE0LDQuNDg5ODE1IC0xMC4xMzk0NSw3LjA2MDY4NyAtOS40NzgyMiwtOS40NzE5MTIgLTIyLjU0OTQ5LC0xNS4zNDk2MDYgLTM2Ljk3MjM5NSwtMTUuMzQ5NjA2IC0xNC40MTU3OTksMCAtMjcuNDg1NTk0LDUuODY5NDkzIC0zNi45NjY5MTcsMTUuMzMzMDg5IEMgNTQuMjkyNzYyLDMzLjQ5MzE1MyA1MC45MDc0MDIsMzEuMTM2ODQ0IDQ3LjM2NTM0MiwyOS4wMDg0NzkgNTkuMTI5MzEsMTYuNDQwMTEzIDc1Ljg2NzI4OCw4LjU5MTgxMSA5NC40NzE2MzUsOC41OTE4MTEgWiBtIDE4OC44Mjc1MDUsMCBjIDE4LjYyNjE5LDAgMzUuMzgyOTUsNy44NjY1MiA0Ny4xNDQ4OCwyMC40NjA2NjEgLTMuNTQxMzEsMi4xMzIyMjEgLTYuOTI4NzEsNC40OTIwODIgLTEwLjEzOTQ1LDcuMDY2MjQzIC05LjQ4MTA2LC05LjQ5MjkyNiAtMjIuNTY0MzUsLTE1LjM4ODE5NSAtMzcuMDA1NDMsLTE1LjM4ODE5NSAtMTQuMzk3NjYsMCAtMjcuNDUwMDMsNS44NTczMjMgLTM2LjkyODQxLDE1LjMwMDA1NiAtMy4yMTUwMSwtMi41NjkzOTggLTYuNjA1NTEsLTQuOTI4MDUgLTEwLjE1MDQxLC03LjA1NTE2OCAxMS43NjI1LC0xMi41NDk5OTcgMjguNDg5OTMsLTIwLjM4MzU5NyA0Ny4wNzg4MiwtMjAuMzgzNTk3IHogbSAtOTQuMzYxNDYsMTUuODIzMzMyIGMgMzYuMTIxNDcsMCA2Ni44NDMwNiwyMi44MzM4NjUgNzguNTMyNjQsNTQuODc3NTY5IC0zLjQ3NzEyLDAuNTk3Njk0IC02Ljg5MzA2LDEuMzg0MTM4IC0xMC4yMjc1NSwyLjM1NzE3OCAtMTAuNTk2MjEsLTI3LjM5MzkwMyAtMzcuMjEwMiwtNDYuODY5NDY4IC02OC4zMDUwOSwtNDYuODY5NDY4IC0zMS4xMDUwNiwwIC01Ny43MjcwMywxOS40ODg2OCAtNjguMzE2MDIsNDYuODk2OTgzIC0zLjMzMzk2LC0wLjk3Njc4MSAtNi43NDUwMiwtMS43NjY3NCAtMTAuMjIyMTUsLTIuMzY4MTc3IDExLjY4NTE2LC0zMi4wNTIyMDggNDIuNDEwOCwtNTQuODk0MDg1IDc4LjUzODE3LC01NC44OTQwODUgeiBtIC05NC40NjYwNDUsNC45MjkyNiBjIDExLjg5MzYwNSwwIDIyLjY1NTAxNSw0LjcyNDgyNSAzMC41Mjg1NjUsMTIuMzk3NjA2IC0yLjY0NDEyLDIuNTUxMzcxIC01LjEzMjE0LDUuMjYzMjU3IC03LjQ1NzI3LDguMTEyNjQzIC01LjkzMTUyLC01Ljg5NTYwOSAtMTQuMDgxNjksLTkuNTYxMTQ2IC0yMy4wNzEyOTUsLTkuNTYxMTQ2IC04Ljk4MDQyMiwwIC0xNy4xMjYxNzQsMy42NTk1NjUgLTIzLjA2MDIyMSw5LjU0NDU5MiAtMi4zMjUxMjcsLTIuODUwMjkzIC00LjgxODMzMSwtNS41NTU0OSAtNy40NjI4MjgsLTguMTA3MTYzIDcuODc2MzQ2LC03LjY2NTk3OCAxOC42MzUxNDksLTEyLjM4NjUzMiAzMC41MjMwNDksLTEyLjM4NjUzMiB6IG0gMTg4LjgyNzUwNSwwIGMgMTEuOTE2MDUsMCAyMi42OTUxNSw0Ljc0MzE1NiAzMC41NzI1MiwxMi40NDE2MzggLTIuNjQyNzIsMi41NTM5MDIgLTUuMTM0MDcsNS4yNjAyNzEgLTcuNDU3Miw4LjExMjYwNSAtNS45MzUyMiwtNS45MjEyNzMgLTE0LjEwMzM0LC05LjYwNTE0IC0yMy4xMTUzMiwtOS42MDUxNCAtOC45NTcyOSwwIC0xNy4wODA1NiwzLjY0MjEwMyAtMjMuMDEwNzEsOS41MDA1OTggLTIuMzI3OTYsLTIuODQ4MjE0IC00LjgyMTMyLC01LjU1NzMwNCAtNy40NjgyNywtOC4xMDcyIDcuODcyNDksLTcuNjQxNTYyIDE4LjYxMjU4LC0xMi4zNDI1MDEgMzAuNDc4OTgsLTEyLjM0MjUwMSB6IG0gLTk0LjM2MTQ2LDEzLjk2NzE2OSBjIDI3LjQ0MzksMCA1MC44NDUxMSwxNy4wMjEyOTEgNjAuMjUyOTYsNDEuMDk3NjM3IC0zLjI0NzU2LDEuMjgyNjU5IC02LjQwNDMzLDIuNzQxODIxIC05LjQ2MjAxLDQuMzY3NDcxIC03Ljc3ODg3LC0yMC40ODc5NDkgLTI3LjYxMjMyLC0zNS4wOTQxOTggLTUwLjc5MDk1LC0zNS4wOTQyNzQgLTIzLjE5NDk3LDcuNmUtNSAtNDMuMDQwMjQsMTQuNjI3MjI1IC01MC44MDc0NCwzNS4xMzg0MTkgLTMuMDU2NzcsLTEuNjI5NTgxIC02LjIxNDksLTMuMDkyMDY5IC05LjQ2MjA4LC00LjM3ODU4MiA5LjM5OTA4LC0yNC4wOTQ1NjQgMzIuODEyODQsLTQxLjEzMDY3MSA2MC4yNjk1MiwtNDEuMTMwNjcxIHogbSAtOTQuNDY2MDQ1LDUuNjAxMjIyIGMgNy4xMjE2NTUsMCAxMy40ODcyNDUsMy4wNTQ3NjUgMTcuODk5Njk1LDcuOTE5OTYyIC0yLjE4MDQ5LDMuMjUyODUgLTQuMTYxNzYsNi42NTA2ODMgLTUuOTIwNzUsMTAuMTc4MDAzIC0yLjIzOTU2LC00LjM2NjUyNiAtNi43NzY0NjMsLTcuMzk2NjQ5IC0xMS45Nzg5NDUsLTcuMzk2NjQ5IC01LjE5NjMyMiwwIC05LjczNzIzNSwzLjAyMjAzNSAtMTEuOTg0NTA0LDcuMzgwMTMzIC0xLjc1NzQ4MSwtMy41Mjc4NDkgLTMuNzMwNDcsLTYuOTI1NTY5IC01LjkwOTY3LC0xMC4xNzgwNDEgNC40MTYyNjUsLTQuODU1Nzg2IDEwLjc4MDQ5OCwtNy45MDM0MDggMTcuODk0MTc0LC03LjkwMzQwOCB6IG0gMTg4LjgyNzUwNSwwIGMgNy4xNDk4NCwwIDEzLjUzNDk0LDMuMDgxNzEzIDE3Ljk0OTIsNy45ODA0NzIgLTIuMTgyOTQsMy4yNjQ2MDUgLTQuMTYyMjgsNi42NzU0NCAtNS45MjA2NywxMC4yMTY1OTIgLTIuMjIxMzgsLTQuNDE5NzA0IC02Ljc4NjgyLC03LjQ5NTc0OCAtMTIuMDI4NTMsLTcuNDk1NzQ4IC01LjE1NjIyLDAgLTkuNjY0NDQsMi45NzcxNzIgLTExLjkyOTQ0LDcuMjgwOTk2IC0xLjc1NzE4LC0zLjUxMTYzNSAtMy43MzQ0LC02Ljg5NTA2OCAtNS45MDk2MywtMTAuMTMzOTcyIDQuNDE0MTEsLTQuODIyMzc1IDEwLjc1MzQ0LC03Ljg0ODM0IDE3LjgzOTA3LC03Ljg0ODM0IHogbSAtOTQuMzYxNDYsMTMuMzAwODc1IGMgMjAuMTUxOTEsMS4xNGUtNCAzNy4yMDU0OCwxMi45MzY4NyA0My4zMzkyLDMwLjk4MDE0NSAtMy4wOTkyMiwyLjAzNzY1NyAtNi4wNzI2MSw0LjI1NDE5OSAtOC45MDAzLDYuNjM2NjI0IC0zLjcwMjA5LC0xNS41ODk2MDcgLTE3Ljc0MzUyLC0yNy4yNTE1MjggLTM0LjQzODksLTI3LjI1MTUyOCAtMTYuNzIyOTgsMCAtMzAuNzg3MjgsMTEuNjk5OTQ0IC0zNC40NjA5MSwyNy4zMjg1OTIgLTIuODI4MjUsLTIuMzg5NDkzIC01Ljc5OTM0LC00LjYxNDYxNCAtOC45MDAyOSwtNi42NTg1ODIgNi4xMTczOSwtMTguMDcyNTY3IDIzLjE4ODY5LC0zMS4wMzUxMzcgNDMuMzYxMiwtMzEuMDM1MjUxIHogbSAtOTQuNDY2MDQ1LDYuMDE5NzI5IGMgMi43MzA4NTksMCA0Ljg1MjE5NSwyLjEzNzg1MiA0Ljg1MjE5NSw0Ljg2ODcxMiAwLDIuNjA0NjYyIC0xLjkyOTcxMyw0LjY1MTg0MyAtNC40Nzc3Miw0LjgzNTY3OSAtMC4xMjUxMDIsLTUuMjllLTQgLTAuMjQ5MDcxLC0wLjAwMzggLTAuMzc0NTUxLC0wLjAwMzggLTAuMTI1MTAyLDAgLTAuMjQ5NDQ5LDAuMDAzOCAtMC4zNzQ1NTEsMC4wMDM4IC0yLjU0OTI1NCwtMC4xODQwNjMgLTQuNDk0MTk5LC0yLjIzMTAxNyAtNC40OTQxOTksLTQuODM1Njc5IDAsLTIuNzMwODYgMi4xMzc4NTIsLTQuODY4NzEyIDQuODY4NzEyLC00Ljg2ODcxMiB6IG0gMTg4LjgyNzUwNSwwIGMgMi43MzA4MiwwIDQuODUyMTUsMi4xMzc4NTIgNC44NTIxNSw0Ljg2ODcxMiAwLDIuNjA0NjYyIC0xLjkyOTc1LDQuNjUxODQzIC00LjQ3NzY0LDQuODM1Njc5IC0wLjEyNTEsLTUuMjllLTQgLTAuMjQ5MDcsLTAuMDAzOCAtMC4zNzQ1NSwtMC4wMDM4IC0wLjEyNTQ4LDAgLTAuMjQ5NDUsMC4wMDM4IC0wLjM3NDU1LDAuMDAzOCAtMi41NDczMywtMC4xODU1NzUgLTQuNDk0MiwtMi4yMzIzNzggLTQuNDk0MiwtNC44MzU2NzkgMCwtMi43MzA4NiAyLjEzNzg1LC00Ljg2ODcxMiA0Ljg2ODcxLC00Ljg2ODcxMiB6IG0gLTk0LjM2MTQ2LDEyLjg4MjIxOCBjIDE0LjU1MjYxLDAgMjYuMzMwMTEsMTEuNDM1OTgxIDI2Ljg2NjAxLDI1Ljg1MjY4NCAtMi43Mzk0LDIuODgzMDYgLTUuMjk4NzUsNS45Mzk4MyAtNy42NTU1NSw5LjE1MzYgLTEuNDY0NDEsLTAuMzExMDUgLTIuOTQ0NTUsLTAuNTg1NzUgLTQuNDMzNjEsLTAuODI2MiAxLjEwMTg5LC0yLjIwODQyIDEuNzI5NDQsLTQuNjg4OTIgMS43Mjk0NCwtNy4zMDg1MSAwLC05LjA2NjkgLTcuNDM5MzYsLTE2LjUwNjI5NiAtMTYuNTA2MjksLTE2LjUwNjI5NiAtOS4wNjY5NCwwIC0xNi41MDYyMiw3LjQzOTM5NiAtMTYuNTA2MjIsMTYuNTA2Mjk2IDAsMi42MTczNiAwLjYxODM3LDUuMTAxNiAxLjcxODM2LDcuMzA4NTEgLTEuNTIwMTMsMC4yNDU2NyAtMy4wMjczMywwLjUyOTA2IC00LjUyMTY4LDAuODQ4MjQgLTIuMzMwNjQsLTMuMTgwOTMgLTQuODU2MzUsLTYuMjA4MDcgLTcuNTYxOTIsLTkuMDY1NTQgMC40Nzk1OCwtMTQuNDY4NzUgMTIuMjgxNjUsLTI1Ljk2Mjc4NCAyNi44NzE0NiwtMjUuOTYyNzg0IHogbSAtOTQuNDY2MDQ1LDUuNDUyNTczIGMgMjYuODQ4NzQ1LDAgNTAuNzIzNTY1LDEyLjQ5NDc3OCA2Ni4yMDExODUsMzEuOTcxNTUxIC0zLjg1MzkxLDEuMjQyMjYgLTcuNTk5MjcsMi43MjY1OSAtMTEuMjEzNDgsNC40NDQ2MSAtMTMuNDA3MDgsLTE1LjMxNDExIC0zMy4wNzM2MywtMjUuMDA5OTYzIC01NC45ODc3NDMsLTI1LjAwOTk2MyAtMjEuOTE1ODU1LDAgLTQxLjU4NzA0OSw5LjY5ODk1MyAtNTQuOTk4NzQsMjUuMDE1NDEzIC0zLjYwOTkwMywtMS43MTc1IC03LjM0NzgxOCwtMy4yMDcyNyAtMTEuMTk2ODUxLC00LjQ1MDA2IEMgNDMuNzUyLDk5LjA2MzgzNiA2Ny42MjM1NzIsODYuNTY4MTg5IDk0LjQ3MTYzNSw4Ni41NjgxODkgWiBtIDE4OC44Mjc1MDUsMCBjIDI2Ljg1OTQ0LDAgNTAuNzQ2MDUsMTIuNTAzNTQ2IDY2LjIyMzE4LDMxLjk5MzQ3MSAtMy44NDk5LDEuMjQ2ODcgLTcuNTg2NzYsMi43NDQ5NiAtMTEuMTk2OTMsNC40NjY3MiAtMTMuNDA3OTEsLTE1LjMzODg3IC0zMy4wOTE3NiwtMjUuMDUzOTkzIC01NS4wMjYyNSwtMjUuMDUzOTkzIC0yMS44OTQ5NiwwIC00MS41NDk0NSw5LjY4MDM5MyAtNTQuOTYwMTksMjQuOTcxMzczIC0zLjYxNDc0LC0xLjcxMzg2IC03LjM1OTU0LC0zLjE5NTIxIC0xMS4yMTM0NCwtNC40MzM2NSAxNS40NzY2MywtMTkuNDU5OTE1IDM5LjMzODM0LC0zMS45NDM5MjEgNjYuMTczNjMsLTMxLjk0MzkyMSB6IG0gLTk0LjM2MTQ2LDEzLjQyNzQ1MiBjIDQuNDU5MDEsMCA3Ljk5MTU0LDMuNTMyNTM5IDcuOTkxNTQsNy45OTE1NDkgMCwyLjU2MDMyIC0xLjE3MTIsNC44MDUyMSAtMy4wMDcxLDYuMjU2NTkgLTEuNjUxNDMsLTAuMDg4NCAtMy4zMTEyOSwtMC4xMzIyOCAtNC45ODQ0NCwtMC4xMzIyOCAtMS42NzE2OSwwIC0zLjMzNDMsMC4wNDk1IC00Ljk4NDM3LDAuMTM3NTcgLTEuODM3MzgsLTEuNDUxMzggLTMuMDA3MTEsLTMuNzAwNjEgLTMuMDA3MTEsLTYuMjYyMTEgMCwtNC40NTkwMSAzLjUzMjQ2LC03Ljk5MTU0NiA3Ljk5MTQ4LC03Ljk5MTU0NiB6IG0gLTk0LjQ2NjA0NSw2LjU5ODE0OSBjIDE4LjY2OTgwNSwwIDM1LjQ2MTUzNSw3LjkwMjk5IDQ3LjIyNzU3NSwyMC41NDg3MiAtMy41NDQ3OCwyLjEyMjYyIC02LjkzNTI0LDQuNDc5MzkgLTEwLjE1MDUyLDcuMDQ0MjkgLTkuNDg2NSwtOS41MzM0NSAtMjIuNjAxMDEsLTE1LjQ1NDM4IC0zNy4wNzcwNTUsLTE1LjQ1NDM4IC0xNC40Nzg2NTIsMCAtMjcuNTk2OTM5LDUuOTIzMzkgLTM3LjA4ODA4OSwxNS40NTk4MiAtMy4yMTM5OTYsLTIuNTY1MDEgLTYuNjAxODUyLC00LjkyMDgzIC0xMC4xNDQ5MzIsLTcuMDQ0MjEgMTEuNzcwNDY5LC0xMi42NDgwNCAyOC41NjEzNjEsLTIwLjU1NDI0IDQ3LjIzMzAyMSwtMjAuNTU0MjQgeiBtIDE4OC44Mjc1MDUsMCBjIDE4LjY5MjIyLDAgMzUuNTAzNTIsNy45MjAzOCA0Ny4yNzE1MywyMC41OTI4MyAtMy41NDI5NywyLjEyNzMxIC02LjkzMTczLDQuNDg2MTEgLTEwLjE0NDkzLDcuMDU1MTcgLTkuNDkwNTgsLTkuNTY0MTMgLTIyLjYyNDA2LC0xNS41MDkzNyAtMzcuMTI2NiwtMTUuNTA5MzcgLTE0LjQ0Nzk3LDAgLTI3LjU0MTE5LDUuODk4MzcgLTM3LjAyNzU0LDE1LjM5OTI3IC0zLjIxODE2LC0yLjU2MjU2IC02LjYxMzk5LC00LjkxMzM1IC0xMC4xNjE0OSwtNy4wMzMyMSAxMS43NjgyNCwtMTIuNjE5OCAyOC41NDExNCwtMjAuNTA0NjkgNDcuMTg5MDMsLTIwLjUwNDY5IHogTSA5NC40NzE2MzUsMTI3LjM0NjI3IGMgMTEuOTQyODE1LDAgMjIuNzQ1NjE1LDQuNzYyMjQgMzAuNjI3NzA1LDEyLjQ5MTMgLTIuNjQ5NDksMi41NDgxOSAtNS4xNDM2LDUuMjU0MjIgLTcuNDczNzksOC4xMDE2NCAtNS45MzgzNiwtNS45NDM5NSAtMTQuMTIyMzksLTkuNjQzODQgLTIzLjE1MzkxNSwtOS42NDM4NCAtOS4wMzM1MjUsMCAtMTcuMjIyMjQ5LDMuNzAzMDcgLTIzLjE2NDkxNCw5LjY0OTI5IC0yLjMzMDY4MywtMi44NDc0NiAtNC44MjQwNzUsLTUuNTUyODUgLTcuNDczNzg5LC04LjEwMTY1IDcuODg2NDc2LC03LjczMTkzIDE4LjY5MzIwMywtMTIuNDk2NzQgMzAuNjM4NzAzLC0xMi40OTY3NCB6IG0gMTg4LjgyNzUwNSwwIGMgMTEuOTczNzMsMCAyMi44MDA5OCw0Ljc4NzMgMzAuNjg4MjUsMTIuNTUxODUgLTIuNjQ3NTIsMi41NTEwNiAtNS4xMzk5Myw1LjI1NzcgLTcuNDY4MzEsOC4xMDcyIC01Ljk0MzUzLC01Ljk4Mjc3IC0xNC4xNTQ4NiwtOS43MDk5NSAtMjMuMjE5OTQsLTkuNzA5OTUgLTguOTk3NzQsMCAtMTcuMTU2NDEsMy42NzI3OSAtMjMuMDkzMjksOS41Nzc2NiAtMi4zMzEyMiwtMi44NDI5NiAtNC44Mjk2NCwtNS41NDY1MyAtNy40NzkzMSwtOC4wOTA1NyA3Ljg4MDk2LC03LjY5NTk4IDE4LjY1ODQ3LC0xMi40MzYxOSAzMC41NzI2LC0xMi40MzYxOSB6IE0gOTQuNDcxNjM1LDE0Ni45MTQ4MSBjIDcuMTU5NTk1LDAgMTMuNTU2Mjk1LDMuMDg2NCAxNy45NzExOTUsNy45OTY5OSAtMi4xODk2MywzLjI1NzU0IC00LjE4MTc4LDYuNjU5NzUgLTUuOTQ4MSwxMC4xOTQ1MiAtMi4yMjI3LC00LjQxNjE5IC02Ljc4Mzk1MiwtNy40OTAzMSAtMTIuMDIzMDk1LC03LjQ5MDMxIC01LjIzOTkzNywwIC05LjgxMjQ0OCwzLjA3MzA2IC0xMi4wMzk2MSw3LjQ5MDMxIC0xLjc2Njg1MywtMy41MzE3OCAtMy43NTg0LC02LjkzMzI0IC01Ljk0ODE0NSwtMTAuMTg5IDQuNDE5NDc3LC00LjkxNDY0IDEwLjgyNDgzMiwtOC4wMDI1MSAxNy45ODc3NTUsLTguMDAyNTEgeiBtIDE4OC44Mjc1MDUsMCBjIDcuMTk5MDUsMCAxMy42MjU2NSwzLjEyMTc4IDE4LjA0Mjg2LDguMDc5NTcgLTIuMTk0NjIsMy4yNzAwNSAtNC4xOTA4Niw2LjY4NTQ2IC01Ljk1OTI2LDEwLjIzMzExIC0yLjE5OTQyLC00LjQ4MTM5IC02Ljc5NjU0LC03LjYxMTQ4IC0xMi4wODM2LC03LjYxMTQ4IC01LjE5MjAyLDAgLTkuNzI5NzksMy4wMTcwMSAtMTEuOTc5MDMsNy4zNjkxNCAtMS43NjQ4MSwtMy41MTgyNSAtMy43NDcwMiwtNi45MDc2NiAtNS45MzE2NiwtMTAuMTUwNTMgNC40MTY4MywtNC44NjU2MSAxMC43ODg1MSwtNy45MTk4MSAxNy45MTA2OSwtNy45MTk4MSB6IG0gLTE4OC44Mjc1MDUsMTkuMzIwNiBjIDIuNzMwODU5LDAgNC44NTIxOTUsMi4xMzc4NiA0Ljg1MjE5NSw0Ljg2ODcyIDAsMi42MDQ2NiAtMS45Mjk3MTMsNC42NTE2OSAtNC40Nzc3Miw0LjgzNTY0IC0wLjEyNTEwMiwtNS4zZS00IC0wLjI0OTA3MSwtMC4wMDQgLTAuMzc0NTUxLC0wLjAwNCAtMC4xMjUxMDIsMCAtMC4yNDk0NDksMC4wMDQgLTAuMzc0NTUxLDAuMDA0IC0yLjU0OTI1NCwtMC4xODQwNyAtNC40OTQxOTksLTIuMjMwOTggLTQuNDk0MTk5LC00LjgzNTY0IDAsLTIuNzMwODYgMi4xMzc4NTIsLTQuODY4NzIgNC44Njg3MTIsLTQuODY4NzIgeiBtIDE4OC44Mjc1MDUsMCBjIDIuNzMwODIsMCA0Ljg1MjE1LDIuMTM3ODYgNC44NTIxNSw0Ljg2ODcyIDAsMi42MDQ2NiAtMS45Mjk3NSw0LjY1MTY5IC00LjQ3NzY0LDQuODM1NjQgLTAuMTI1MSwtNS4zZS00IC0wLjI0OTA3LC0wLjAwNCAtMC4zNzQ1NSwtMC4wMDQgLTAuMTI1NDgsMCAtMC4yNDk0NSwwLjAwNCAtMC4zNzQ1NSwwLjAwNCAtMi41NDczMywtMC4xODU1OCAtNC40OTQyLC0yLjIzMjM4IC00LjQ5NDIsLTQuODM1NjQgMCwtMi43MzA4NiAyLjEzNzg1LC00Ljg2ODcyIDQuODY4NzEsLTQuODY4NzIgeiBNIDAsMTcwLjc3OTE2IHYgOC41MzY3MSBjIDExLjI1NzczOSwwIDIwLjgyODU5OCw2Ljg1OTU4IDI0Ljg0NDgsMTYuNjMyOSBoIDguOTk5MzU3IEMgMjkuNDMxMTgxLDE4MS40MTkyOSAxNS45Mzk3OCwxNzAuNzc5MTYgMCwxNzAuNzc5MTYgWiBtIDM3Ny44NzUzMSwwIGMgLTE1LjkzOTc4LDAgLTI5LjQzNjM5LDEwLjY0MDAxIC0zMy44NDk2MywyNS4xNjk2MSBoIDkuMDA0OTUgYyA0LjAxNTYzLC05Ljc3NDg0IDEzLjU4NTc3LC0xNi42MzI5IDI0Ljg0NDY4LC0xNi42MzI5IHogTSAwLDE4OS42ODExMSB2IDYuMjY3NjYgSCAxMi45MDQzMjggQyA5Ljg3MTc0OCwxOTIuMTQxMDUgNS4yMDgxODksMTg5LjY4MTExIDAsMTg5LjY4MTExIFogbSAzNzcuODc1MzEsMCBjIC01LjIwODExLDAgLTkuODcxNjMsMi40NTk5NCAtMTIuOTA0MjEsNi4yNjc2NiBoIDEyLjkwNDIxIHoiIC8+CiAgICA8L3BhdHRlcm4+CiAgICA8Y2xpcFBhdGgKICAgICAgIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIgogICAgICAgaWQ9ImNsaXBQYXRoNTQiPgogICAgICA8cGF0aAogICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICBzdHlsZT0ic3Ryb2tlLXdpZHRoOjAuMDk5OTk5OTtzdHJva2UtbGluZWNhcDpzcXVhcmU7cGFpbnQtb3JkZXI6bWFya2VycyBmaWxsIHN0cm9rZTtzdG9wLWNvbG9yOiMwMDAwMDAiCiAgICAgICAgIGQ9Im0gMTY5MS4xOTk0LC03OTIuMzIwMDEgaCAxNjM0Ljg5MTkgdiA5Mi44MjgwNiBIIDE2OTEuMTk5NCBaIiAvPgogICAgPC9jbGlwUGF0aD4KICA8L2RlZnM+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIGQ9Ik0gMjUuMDAwMDI0LDQuOWUtNSBBIDI0Ljk5OTk1MSwyNC45OTk5NTEgMCAwIDEgNC45ZS01LDI1LjAwMDAyNCAyNC45OTk5NTEsMjQuOTk5OTUxIDAgMCAxIDI1LjAwMDAyNCw1MCAyNC45OTk5NTEsMjQuOTk5OTUxIDAgMCAxIDUwLDI1LjAwMDAyNCAyNC45OTk5NTEsMjQuOTk5OTUxIDAgMCAxIDI1LjAwMDAyNCw0LjllLTUgWiIKICAgICAgIHN0eWxlPSJmaWxsOnVybCgjcGF0dGVybjE4KTtzdHJva2Utd2lkdGg6NS4yOTE2NTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7cGFpbnQtb3JkZXI6c3Ryb2tlIGZpbGwgbWFya2VycztzdHJva2U6bm9uZTtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMyIgLz4KICA8L2c+Cjwvc3ZnPgo=
// @require https://update.greasyfork.org/scripts/450160/1704233/WME-Bootstrap.js
// ==/UserScript==
/* global require */
/* global $, jQuery */
/* global I18n */
/* global Node$1, Segment, Venue, VenueAddress, WmeSDK */
/* global W, WazeWrap, XLSX, WazeToastr */
(function () {
'use strict';
let permalinks = [];
let currentIndex = -1;
let allWorkflows = {};
let isLooping = false; // Biến này chỉ ra rằng vòng lặp đang hoạt động hoặc được yêu cầu dừng
let workbookData = null; // Lưu workbook để ghi đè
let currentFileName = ''; // Tên file hiện tại
let statusColumnIndex = -1; // Vị trí cột Status
let hasUnsavedChanges = false;
let previousIndex = -1;
let currentRowData = []; // Lưu dữ liệu thô của hàng hiện tại trong Excel
let eventCleanupRegistry = [];
let wmeSDK = null;
let batchRunCounter = 0;
const STORAGE_KEY_SETTINGS = 'wme_wfe_settings';
const SETTINGS_IDS = [
'url_column', 'gas_url', 'sheet_name_input', 'url_col_name',
'coordinate_zoom', 'save_status_enabled', 'save_permalink_after_create',
'poi_category_select', 'workflow_select', 'workflow_variable_input',
'skip_done_check', 'batch_save_enabled', 'batch_save_limit'
];
const STATUS_COL_NAME = 'Status';
let isGasMode = false;
let gasHeaders = null;
let selectedSubCategory = 'CAR_WASH';
const PROVIDERS = {
//['', 'MIPECORP', 'PV Oil', 'Petrolimex', 'SaigonPetro', 'Satra', 'Thalexim']
petrolimex: {
brand: 'Petrolimex',
url: 'petrolimex.com.vn',
phone: '1900 2828'
},
saigonpetro: {
brand: 'SaigonPetro',
url: 'saigonpetro.com.vn',
},
pvoil: {
brand: 'PV Oil',
url: 'pvoil.com.vn'
},
mipecorp: {
brand: 'MIPECORP',
url: 'mipecorp.com.vn'
},
evone: {
brand: 'ev-one.vn'
}
}
const networkMapping = {
"vinfast": "Vinfast (V-Green)",
"rabbitevc": "Rabbit EVC",
"evone": "EVOne",
"charge+": "Charge+",
"default": "Other"
};
const SDK_REGISTRY = {
"update_charge_station": {
name: "Thêm trạm sạc",
description: "Thêm trạm sạc vào WME",
params: [
{
key: "name",
label: "Cột chứa tên các cửa hàng nhượng quyền",
placeholder: "{{A}} (Optional)"
},
{
key: "provider",
label: "Cột chứa tên nhà cung cấp dịch vụ trạm sạc",
placeholder: "{{K}} (Optional)"
},
{
key: "openHours",
label: "Cột giờ mở cửa (vd: 07:00 SA - 05:00 CH)",
placeholder: "{{E}} (Optional)"
},
{
key: "phone",
label: "Cột chứa số điện thoại",
placeholder: "{{M}} (Optioinal)"
},
{
key: "url",
label: "Cột chứa địa chỉ trang web",
},
{
key: "address",
label: "Cột địa chỉ",
placeholder: "{{C}} (Optional)"
},
{
key: "numberCharge",
label: "Cột chứa số lượng cổng sạc",
placeholder: "{{O}} (Optional)"
},
{
key: "power",
label: "Cột công suất của trạm sạc (sep=',')",
placeholder: "{{L}} (Optional)"
}
]
},
"update_gas_station": {
name: "Cập nhật Thông tin trạm xăng",
description: "Dựa vào {{tên cột}} để lấy tên cửa hàng trong bảng tính",
params: [
{
key: 'name',
label: 'Tên cột chứa tên cửa hàng',
placeholder: "{{A}}"
},
{
key: "provider",
label: "Tên nhà cung cấp viết liền, không viết hoa (petrolimex,saigonpetro,...)"
},
{
key: "openHours",
label: "Cột giờ mở cửa (vd: 07:00 SA - 05:00 CH)",
placeholder: "{{F}} (Optional)"
},
{
key: "phone",
label: "Cột số điện thoại (vd: 09000..00)",
placeholder: "{{C}} (Optional)"
}
]
},
"update_lock_rank": {
name: "Khóa đối tượng (Lock Level)",
description: "Đặt cấp độ khóa cho đối tượng.",
params: [
{ key: "rank", label: "Cấp độ (1-5)", type: "number", min: 1, max: 5, placeholder: "3" }
]
},
"update_segment_city": {
name: "Cập nhật tên tỉnh,tp/xã phường mới cho đường",
description: "Đổi tên tỉnh,tp/xã phường mới cho các Segment đang chọn. Dùng {{value}} để lấy từ ô nhập liệu.",
params: [
{ key: "cityName", label: "Tên TP mới", placeholder: "{{value}}" }
]
},
};
const defaultWorkflows = {
"update_charge_station": {
name: "Cập nhật dữ liệu trạm sạc",
tasks: [
{
taskId: "update_charge_station",
enabled: true,
params: {
name: "{{name}}",
provider: "{{provider}}",
openHours: "{{open hours}}",
address: "{{address}}",
power: "{{power}}",
url: "{{Url}}",
phone: "{{Phone}}",
numberCharge: "{{NumberCharge}}"
}
},
]
},
"wf_update_gas_saition": {
name: "Cập nhật dữ liệu trạm xăng",
tasks: [
{
taskId: "update_gas_station",
enabled: true,
params: {
name: "{{A}}",
provider: "Petrolimex",
openHours: "{{F}}",
phone: "{{C}}"
}
}
]
},
"wf_update_segment_city": {
name: "Đổi tên tỉnh,tp/xã phường mới cho Segments",
tasks: [
{
taskId: "update_segment_city",
enabled: true,
params: { cityName: "{{value}}" }
}
]
}
};
let CATEGORIES = [
{ key: 'CAR_SERVICES', subs: ['CAR_WASH', 'CHARGING_STATION', 'GARAGE_AUTOMOTIVE_SHOP', 'GAS_STATION'] },
{ key: 'CRISIS_LOCATIONS', subs: ['DONATION_CENTERS', 'SHELTER_LOCATIONS'] },
{
key: 'CULTURE_AND_ENTERTAINEMENT',
subs: ['ART_GALLERY', 'CASINO', 'CLUB', 'TOURIST_ATTRACTION_HISTORIC_SITE', 'MOVIE_THEATER', 'MUSEUM', 'MUSIC_VENUE', 'PERFORMING_ARTS_VENUE', 'GAME_CLUB', 'STADIUM_ARENA', 'THEME_PARK', 'ZOO_AQUARIUM', 'RACING_TRACK', 'THEATER'],
},
{ key: 'FOOD_AND_DRINK', subs: ['RESTAURANT', 'BAKERY', 'DESSERT', 'CAFE', 'FAST_FOOD', 'FOOD_COURT', 'BAR', 'ICE_CREAM'] },
{ key: 'LODGING', subs: ['HOTEL', 'HOSTEL', 'CAMPING_TRAILER_PARK', 'COTTAGE_CABIN', 'BED_AND_BREAKFAST'] },
{ key: 'NATURAL_FEATURES', subs: ['ISLAND', 'SEA_LAKE_POOL', 'RIVER_STREAM', 'FOREST_GROVE', 'FARM', 'CANAL', 'SWAMP_MARSH', 'DAM'] },
{ key: 'OTHER', subs: ['CONSTRUCTION_SITE'] },
{ key: 'OUTDOORS', subs: ['PARK', 'PLAYGROUND', 'BEACH', 'SPORTS_COURT', 'GOLF_COURSE', 'PLAZA', 'PROMENADE', 'POOL', 'SCENIC_LOOKOUT_VIEWPOINT', 'SKI_AREA'] },
{ key: 'PARKING_LOT', subs: ['PARKING_LOT'] },
{
key: 'PROFESSIONAL_AND_PUBLIC',
subs: [
'COLLEGE_UNIVERSITY',
'SCHOOL',
'CONVENTIONS_EVENT_CENTER',
'GOVERNMENT',
'LIBRARY',
'CITY_HALL',
'ORGANIZATION_OR_ASSOCIATION',
'PRISON_CORRECTIONAL_FACILITY',
'COURTHOUSE',
'CEMETERY',
'FIRE_DEPARTMENT',
'POLICE_STATION',
'MILITARY',
'HOSPITAL_URGENT_CARE',
'DOCTOR_CLINIC',
'OFFICES',
'POST_OFFICE',
'RELIGIOUS_CENTER',
'KINDERGARDEN',
'FACTORY_INDUSTRIAL',
'EMBASSY_CONSULATE',
'INFORMATION_POINT',
'EMERGENCY_SHELTER',
'TRASH_AND_RECYCLING_FACILITIES',
],
},
{
key: 'SHOPPING_AND_SERVICES',
subs: [
'ARTS_AND_CRAFTS',
'BANK_FINANCIAL',
'SPORTING_GOODS',
'BOOKSTORE',
'PHOTOGRAPHY',
'CAR_DEALERSHIP',
'FASHION_AND_CLOTHING',
'CONVENIENCE_STORE',
'PERSONAL_CARE',
'DEPARTMENT_STORE',
'PHARMACY',
'ELECTRONICS',
'FLOWERS',
'FURNITURE_HOME_STORE',
'GIFTS',
'GYM_FITNESS',
'SWIMMING_POOL',
'HARDWARE_STORE',
'MARKET',
'SUPERMARKET_GROCERY',
'JEWELRY',
'LAUNDRY_DRY_CLEAN',
'SHOPPING_CENTER',
'MUSIC_STORE',
'PET_STORE_VETERINARIAN_SERVICES',
'TOY_STORE',
'TRAVEL_AGENCY',
'ATM',
'CURRENCY_EXCHANGE',
'CAR_RENTAL',
'TELECOM',
],
},
{
key: 'TRANSPORTATION',
subs: ['AIRPORT', 'BUS_STATION', 'FERRY_PIER', 'SEAPORT_MARINA_HARBOR', 'SUBWAY_STATION', 'TRAIN_STATION', 'BRIDGE', 'TUNNEL', 'TAXI_STATION', 'JUNCTION_INTERCHANGE', 'REST_AREAS', 'CARPOOL_SPOT'],
},
];
const STORAGE_KEY = 'wme_custom_workflows';
function bootstrap() {
if (typeof WazeWrap !== 'undefined' && WazeWrap.Init) {
WazeWrap.Init(() => {
const sdk = typeof unsafeWindow !== 'undefined' && unsafeWindow.getWmeSdk ? unsafeWindow.getWmeSdk({ scriptId: 'wme-wfe', scriptName: 'WME Workflow Engine' }) : getWmeSdk({ scriptId: 'wme-wfe', scriptName: 'WME Workflow Engine' });
init(sdk);
});
} else {
if (typeof unsafeWindow !== 'undefined' && unsafeWindow.SDK_INITIALIZED) {
unsafeWindow.SDK_INITIALIZED.then(() => {
const sdk = unsafeWindow.getWmeSdk({ scriptId: 'wme-wfe', scriptName: 'WME Workflow Engine' });
init(sdk);
});
} else if (typeof window.SDK_INITIALIZED !== 'undefined') {
window.SDK_INITIALIZED.then(() => {
const sdk = window.getWmeSdk({ scriptId: 'wme-wfe', scriptName: 'WME Workflow Engine' });
init(sdk);
})
} else {
log('WME SDK is not available. Script will not run.', 'error');
}
}
}
async function init(sdk) {
console.log("WME Workflow Engine: Initialized");
wmeSDK = sdk
loadWorkflows();
await createUI();
bindSidebarEvents();
loadAllSettings();
updateUIState();
registerHotkeys();
window.addEventListener('beforeunload', (e) => {
cleanupAllEvents();
placeholderCache.clear();
if (hasUnsavedChanges) {
const message = 'Bạn có thay đổi status chưa được lưu! Nhấn "Cập nhật Status" trước khi thoát.';
e.preventDefault();
return message;
}
});
}
function registerEventCleanup(element, event, handler) {
element.addEventListener(event, handler);
eventCleanupRegistry.push({ element, event, handler });
}
function cleanupAllEvents() {
eventCleanupRegistry.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
eventCleanupRegistry = [];
}
function resetData() {
if (window._wmeWorkflowInterval) {
clearInterval(window._wmeWorkflowInterval);
window._wmeWorkflowInterval = null;
}
if (window._wmeWorkflowTimeout) {
clearTimeout(window._wmeWorkflowTimeout);
window._wmeWorkflowTimeout = null;
}
cleanupAllEvents();
if (workbookData) {
workbookData = null;
}
permalinks.length = 0;
currentRowData = null;
batchRunCounter = 0;
const logBox = document.getElementById('log_info');
if (logBox) {
logBox.innerHTML = '';
}
}
/**
* Tạo POI mới trên bản đồ Waze
* @param {number} lat - Vĩ độ
* @param {number} lon - Kinh độ
* @param {string} type - 'point' hoặc 'area'
*/
async function createWazePOI(lat, lon, type, method = 'auto') {
try {
let geometry;
if (method === 'auto') {
if (!lat || !lon) return;
if (type === 'point') {
geometry = { type: "Point", coordinates: [lon, lat] };
} else {
const offset = 0.00015;
geometry = {
type: "Polygon",
coordinates: [[[lon - offset, lat - offset], [lon + offset, lat - offset], [lon + offset, lat + offset], [lon - offset, lat + offset], [lon - offset, lat - offset]]]
};
}
} else {
geometry = await (type === 'point' ? wmeSDK.Map.drawPoint() : wmeSDK.Map.drawPolygon());
}
const newId = wmeSDK.DataModel.Venues.addVenue({
category: selectedSubCategory,
geometry: geometry
});
setTimeout(() => {
wmeSDK.Editing.setSelection({ selection: { ids: [newId.toString()], objectType: 'venue' } });
}, 100)
} catch (err) {
log(`Lỗi tạo POI: ${err.message}`, 'error');
}
}
let placeholderCache = new Map();
function replacePlaceholders(text) {
if (!text || typeof text !== 'string') return text;
const cacheKey = `${text}_${currentIndex}`;
if (placeholderCache.has(cacheKey)) {
return placeholderCache.get(cacheKey);
}
if (!currentRowData || typeof currentRowData !== 'object') {
return text.replace(/{{[A-Z]+}}/g, match => match)
.replace(/{{[^}]+}}/g, match => match);
}
let result = text.replace(/{{([^}]+)}}/g, (match, key) => {
const trimmedKey = key.trim();
if (currentRowData[trimmedKey] !== undefined) {
return currentRowData[trimmedKey];
}
return match;
});
const manualValue = document.getElementById('workflow_variable_input').value;
result = result.replace('{{value}}', manualValue);
placeholderCache.set(cacheKey, result);
if (placeholderCache.size > 100) {
const firstKey = placeholderCache.keys().next().value;
placeholderCache.delete(firstKey);
}
return result;
}
function getColumnLetter(colIndex) {
let temp, letter = '';
while (colIndex >= 0) {
temp = colIndex % 26;
letter = String.fromCharCode(temp + 65) + letter;
colIndex = Math.floor(colIndex / 26) - 1;
}
return letter;
}
function getColumnIndexFromLetter(colLetter) {
let colIndex = 0;
for (let i = 0; i < colLetter.length; i++) {
colIndex = colIndex * 26 + (colLetter.charCodeAt(i) - 64);
}
return colIndex - 1;
}
/**
* Tìm một phần tử, hỗ trợ tìm kiếm bên trong Shadow DOM.
* @param {string} selector - CSS selector cho phần tử chính.
* @param {string} [shadowSelector] - CSS selector cho phần tử bên trong shadow DOM.
* @returns {Promise<Element|null>}
*/
async function findElement(selector, shadowSelector = '') {
try {
const baseElement = await waitForElement(selector);
if (!shadowSelector) {
return baseElement;
}
if (baseElement && baseElement.shadowRoot) {
await delay(50);
const shadowElement = baseElement.shadowRoot.querySelector(shadowSelector);
if (!shadowElement) {
log(`Lỗi: Không tìm thấy phần tử con với selector "${shadowSelector}" trong shadow DOM của "${selector}".`, 'error');
}
return shadowElement;
}
log(`Lỗi: Không tìm thấy shadow root trên phần tử "${selector}".`, 'error');
return null;
} catch (error) {
log(`Lỗi khi tìm phần tử "${selector}": ${error.message}`, 'error');
throw error;
}
}
async function updateField(baseSelector, shadowSelector, newValue) {
try {
const host = document.querySelector(baseSelector);
if (!host?.shadowRoot) return false;
const input = shadowSelector.includes('textarea')
? host.shadowRoot.querySelector('textarea')
: host.shadowRoot.querySelector(shadowSelector);
if (!input) return false;
if (input.value === newValue) return true;
input.value = newValue;
const eventData = { bubbles: true, composed: true };
input.dispatchEvent(new Event("input", eventData));
input.dispatchEvent(new Event("change", eventData));
return true;
} catch (e) {
return false;
}
}
function findCityIdByName(cityName) {
if (!cityName) return null;
const targetName = cityName.toString().trim();
const cities = W.model.cities.objects;
for (const id in cities) {
if (cities.hasOwnProperty(id)) {
const city = cities[id];
if (city.attributes && city.attributes.name === targetName) {
return city.attributes.id;
}
}
}
return null;
}
function getOrCreateStreet(streetName, cityId) {
return wmeSDK.DataModel.Streets.getStreet({ streetName, cityId })
?? wmeSDK.DataModel.Streets.addStreet({ streetName, cityId });
}
function convertTo24Hour(timeStr) {
const parts = timeStr.trim().split(' ');
if (parts.length !== 2) return null;
let [hourMin, meridiem] = parts;
let [hourStr, minuteStr] = hourMin.split(':');
const hour = parseInt(hourStr);
const minute = parseInt(minuteStr);
if (isNaN(hour) || isNaN(minute)) return null;
meridiem = meridiem.toUpperCase();
let hour24 = hour;
if (meridiem === 'SA') {
if (hour === 12) hour24 = 0; // 12:xx SA -> 00:xx
} else if (meridiem === 'CH') {
if (hour !== 12) hour24 += 12; // 01:xx CH -> 13:xx
} else {
return null;
}
return `${String(hour24).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
}
/**
* Parse giờ mở cửa từ định dạng Việt Nam sang cấu trúc WME SDK.
* @param {string} openHoursString Ví dụ: "07:00 SA - 05:00 CH" hoặc "24/24", "24/7"
* @returns {Array<Object>|null} WME openingHours array hoặc null nếu lỗi.
*/
function parseVietnameseOpenHours(openHoursString) {
if (!openHoursString) return null;
openHoursString = openHoursString.toString().trim().toUpperCase();
if (openHoursString === '24/24' || openHoursString === '24/7') {
return [{ days: [0, 1, 2, 3, 4, 5, 6], fromHour: "00:00", toHour: "00:00" }];
}
const match = openHoursString.match(/(\d{1,2}:\d{2}\s+(?:SA|CH))\s*-\s*(\d{1,2}:\d{2}\s+(?:SA|CH))/);
if (!match) {
log(`Không thể parse giờ mở cửa: "${openHoursString}".`, 'warn');
return null;
}
const from24h = convertTo24Hour(match[1]);
const to24h = convertTo24Hour(match[2]);
if (from24h && to24h) {
return [{ days: [0, 1, 2, 3, 4, 5, 6], fromHour: from24h, toHour: to24h }];
} else {
log(`Lỗi chuyển đổi giờ từ "${openHoursString}" sang 24h.`, 'error');
return null;
}
}
function capitalizeWords(string) {
const words = string.split(' ');
const capitalizedWords = words.map(word => {
if (word.length === 0) return '';
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
});
return capitalizedWords.join(' ');
}
const taskHandlers = {
update_charge_station: async (parsedParams, selectedFeature) => {
const featureModel = selectedFeature?.WW?.getObjectModel ? selectedFeature.WW.getObjectModel() : null;
if (!featureModel) {
return false;
}
const venueId = featureModel.attributes.id;
const providerRaw = (parsedParams.provider || "").toString();
const provider = providerRaw.toLowerCase();
const nameAttribute = parsedParams.name || "";
const finalName = `Trạm sạc ${providerRaw} - ${nameAttribute}`.replace(/Cửa hàng xăng dầu/gi, "CHXD");
const currentAttrs = featureModel.attributes.categoryAttributes || {};
const newAttrs = JSON.parse(JSON.stringify(currentAttrs));
const powerList = String(parsedParams.power || "").split(",").map(p => parseFloat(p.trim())).filter(p => !isNaN(p));
const countList = String(parsedParams.numberCharge || "").split(",").map(c => parseInt(c.trim()) || 1);
const portMap = new Map();
powerList.forEach((pVal, index) => {
const typeID = pVal >= 30 ? 'CCS_TYPE2' : 'TYPE2';
const key = `${typeID}_${pVal}`;
const currentCount = countList[index] || 1;
if (portMap.has(key)) {
portMap.get(key).count += currentCount;
} else {
portMap.set(key, {
portId: `${typeID}.${pVal}`,
connectorTypes: [typeID],
maxChargeSpeedKw: Math.round(pVal),
count: currentCount
});
}
});
newAttrs.CHARGING_STATION = {
chargingPorts: Array.from(portMap.values()),
accessType: "PUBLIC",
paymentMethods: ["CREDIT", "APP", "DEBIT", "ONLINE_PAYMENT"],
costType: "FEE",
network: networkMapping[provider] || networkMapping.default,
locationInVenue: parsedParams.address || ""
};
const updatePayload = {
venueId,
name: finalName,
phone: parsedParams.phone,
url: parsedParams.url,
categoryAttributes: newAttrs,
};
if (parsedParams.openHours) {
const hours = parseVietnameseOpenHours(parsedParams.openHours);
if (hours) updatePayload.openingHours = hours;
}
wmeSDK.DataModel.Venues.updateVenue(updatePayload);
await delay(200);
wmeSDK.DataModel.Venues.updateVenue(updatePayload);
return true;
},
update_lock_rank: async (parsedParams, selectedFeature) => {
const featureModel = selectedFeature?.WW?.getObjectModel ? selectedFeature.WW.getObjectModel() : null;
if (!featureModel) {
return false;
}
const rank = parseInt(parsedParams.rank) || 1;
const modelRank = Math.max(0, Math.min(5, rank - 1));
const WazeActionUpdateObject = require("Waze/Action/UpdateObject");
if (featureModel.attributes.lockRank !== modelRank) {
W.model.actionManager.add(new WazeActionUpdateObject(featureModel, { lockRank: modelRank }));
}
return true;
},
update_segment_city: async (parsedParams) => {
const cityID = findCityIdByName(parsedParams.cityName);
const city = W.model.cities.objects[cityID].attributes;
const segmentsSelected = wmeSDK.Editing.getSelection();
segmentsSelected?.ids.forEach(segmentId => {
const newCityProperties = {
cityName: city.name,
countryId: cityID,
};
let newCityId = wmeSDK.DataModel.Cities.getById({ cityId: cityID })?.id;
if (newCityId == null) {
newCityId = wmeSDK.DataModel.Cities.addCity(newCityProperties).id;
}
const currentStreetName = wmeSDK.DataModel.Segments.getAddress({ segmentId }).street.name;
const newPrimaryStreetId = getOrCreateStreet(currentStreetName, newCityId).id;
wmeSDK.DataModel.Segments.updateAddress({ segmentId, primaryStreetId: newPrimaryStreetId });
});
return true;
},
update_gas_station: async (parsedParams, selectedFeature) => {
const featureModel = selectedFeature?.WW?.getObjectModel ? selectedFeature.WW.getObjectModel() : null;
if (!featureModel) {
return false;
}
const venueSelected = wmeSDK.Editing.getSelection();
const venueId = venueSelected.ids[0];
const providerKey = (parsedParams.provider || '').trim().toLowerCase();
const providerConfig = PROVIDERS[providerKey];
if (!providerConfig) {
log(`Provider "${parsedParams.provider}" chưa được hỗ trợ.`, 'warn');
}
let name = parsedParams.name.replace(/Cửa hàng xăng dầu/gi, "CHXD");
if (name.includes(parsedParams.provider)) {
name = name.replace(new RegExp(parsedParams.provider, "gi"), "").trim();
}
const updatePayload = {
venueId,
brand: providerConfig?.brand || parsedParams.provider.toString(),
name: `${providerConfig?.brand || parsedParams.provider} - ${name}`,
phone: providerConfig?.phone || '',
categories: [selectedSubCategory],
url: providerConfig?.url || '',
};
if (parsedParams.phone?.toString().trim()) {
updatePayload.phone = parsedParams.phone.toString().trim();
}
if (parsedParams.openHours) {
const hours = parseVietnameseOpenHours(parsedParams.openHours);
if (hours) {
updatePayload.openingHours = hours;
} else {
log(`SDK: Bỏ qua giờ mở cửa do parse lỗi.`, 'warn');
}
}
wmeSDK.DataModel.Venues.updateVenue(updatePayload);
return true;
}
};
async function executeSdkTask(task, selectedFeature) {
const parsedParams = Object.fromEntries(
Object.entries(task.params).map(([key, val]) => [key, replacePlaceholders(val)])
);
const handler = taskHandlers[task.taskId];
if (!handler) {
log(`❌ Task "${task.taskId}" không được hỗ trợ.`, 'error');
return false;
}
const result = await handler(parsedParams, selectedFeature);
await delay(100);
return result;
}
async function runSelectedWorkflow(isCalledByLoop = false) {
const workflowId = document.getElementById('workflow_select').value;
const item = permalinks[currentIndex];
if (item) {
const createMode = document.querySelector('input[name="poi_creation_mode"]:checked')?.value || 'none';
const method = document.querySelector('input[name="poi_method"]:checked')?.value || 'auto';
const coords = extractCoords(item);
if (createMode !== 'none' && coords && !item.url.includes('venues=')) {
await createWazePOI(coords.lat, coords.lon, createMode, method);
await delay(300);
} else if (item.url.includes('segments=') || item.url.includes('venues=')) {
await parseWazeUrlAndNavigate(item.url, false);
await delay(1200);
}
}
if (!workflowId || !allWorkflows[workflowId]) {
log("Hệ thống: Không có workflow nào được chọn để thực thi sau khi tạo/chọn object.", "info");
return;
}
const workflow = allWorkflows[workflowId];
const selection = WazeWrap.getSelectedFeatures();
if (selection.length === 0) {
log("❌ Chưa chọn đối tượng nào trên bản đồ để chạy Workflow Tasks!", "error");
if (isCalledByLoop) throw new Error("Không có selection.");
return;
}
const target = selection[0];
try {
const tasksToRun = (workflow.tasks || []).filter(t => t.enabled);
if (tasksToRun.length === 0) {
log("Workflow không có hành động nào được bật.", "warn");
return;
}
for (const task of tasksToRun) {
if (isCalledByLoop && !isLooping) throw new Error("Stopped by user");
await executeSdkTask(task, target);
}
} catch (error) {
log(`Lỗi Workflow: ${error.message}`, 'error');
console.error(error);
throw error;
}
if (!isCalledByLoop) {
await handleBatchSave();
}
}
async function toggleWorkflowLoop() {
if (isLooping) {
isLooping = false;
log("Đã yêu cầu dừng vòng lặp. Sẽ dừng sau khi hoàn thành hoặc giữa bước hiện tại.", 'warn');
} else {
if (permalinks.length === 0) {
log("Vui lòng tải một file Excel/CSV trước khi bắt đầu vòng lặp.", 'warn');
return;
}
isLooping = true;
updateUIState();
log("--- Bắt đầu vòng lặp tự động ---", 'special');
await executeLoop();
}
}
async function executeLoop() {
if (currentIndex < 0 && permalinks.length > 0) {
currentIndex = 0;
}
while (isLooping && currentIndex < permalinks.length) {
updateUIState();
updateStatus('Đang tạo');
try {
await processCurrentLink();
if (!isLooping) { break; }
await delay(1000);
if (!isLooping) { break; }
await runSelectedWorkflow(true);
await handleBatchSave();
const shouldSavePermalink = document.getElementById('save_permalink_after_create')?.checked;
if (shouldSavePermalink) {
const newPermalink = wmeSDK.Map.getPermalink();
if (newPermalink) {
updatePermalinkInWorkbook(currentIndex, newPermalink);
} else {
log(`⚠️ (Loop) Không lấy được Permalink (Đối tượng chưa được lưu hoặc chưa có ID).`, 'warn');
}
}
updateStatus('Đã tạo');
} catch (error) {
if (isLooping) {
log(`Lỗi ở mục ${currentIndex + 1}, bỏ qua và tiếp tục. Lỗi: ${error.message}`, 'error');
} else {
log(`Vòng lặp đã dừng tại mục ${currentIndex + 1} do yêu cầu dừng.`, 'warn');
}
break;
}
if (!isLooping) break;
if (currentIndex < permalinks.length - 1) {
previousIndex = currentIndex;
currentIndex++;
if (!isLooping) { break; }
await delay(100);
} else {
log("Đã đến mục cuối cùng của danh sách.", 'info');
isLooping = false;
}
}
isLooping = false;
if (currentIndex >= permalinks.length && permalinks.length > 0) {
log("--- ✅ Hoàn thành vòng lặp tự động! ---", 'special');
} else if (permalinks.length === 0) {
log("Không có permalink nào để lặp.", 'warn');
} else {
log("--- Vòng lặp tự động đã dừng. ---", 'warn');
}
updateUIState();
}
function handleFile(e) {
isGasMode = false;
gasHeaders = null;
resetData()
permalinks = [];
currentIndex = -1;
previousIndex = -1;
hasUnsavedChanges = false;
const file = e.target.files[0];
if (!file) {
updateUIState();
return;
}
currentFileName = file.name;
const urlColumnInput = document.getElementById('url_column').value.toUpperCase();
const urlColumnIndex = getColumnIndexFromLetter(urlColumnInput);
if (urlColumnIndex < 0 || urlColumnIndex > 255) {
log(`Lỗi: Cột "${urlColumnInput}" không hợp lệ. Vui lòng nhập A-IV.`, 'error');
updateUIState();
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array', cellDates: false, cellStyles: false });
workbookData = workbook;
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const json = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '', raw: false });
if (json.length === 0) {
log('File không có dữ liệu.', 'error');
updateUIState();
return;
}
const headerRow = json[0];
statusColumnIndex = headerRow.findIndex(h => h && h.toString().trim().toLowerCase() === STATUS_COL_NAME.toLowerCase());
let hasExistingStatus = statusColumnIndex !== -1;
if (!hasExistingStatus) {
statusColumnIndex = headerRow.length;
headerRow.push(STATUS_COL_NAME);
}
const headersMap = headerRow.map(h => h.toString().trim() || null);
const permalinkData = [];
let foundWorkingIndex = -1;
for (let i = 1; i < json.length; i++) {
const rawRow = json[i];
while (rawRow.length < statusColumnIndex + 1) {
rawRow.push('');
}
const cellValue = rawRow[urlColumnIndex];
if (cellValue && typeof cellValue === 'string') {
const trimmedValue = cellValue.trim();
const isURL = trimmedValue.includes('waze.com/editor') ||
trimmedValue.includes('waze.com/ul');
const isCoordinate = /^\s*\(?\s*-?\d+\.?\d*\s*,\s*-?\d+\.?\d*\s*\)?\s*$/.test(trimmedValue);
if (isURL || isCoordinate) {
const rowObject = {};
for (let idx = 0; idx < rawRow.length; idx++) {
const headerName = headersMap[idx];
const val = rawRow[idx];
if (headerName) {
rowObject[headerName] = val;
}
rowObject[getColumnLetter(idx)] = val;
}
const status = rawRow[statusColumnIndex].toString().trim();
permalinkData.push({
url: trimmedValue,
rowIndex: i,
status: status,
rowData: rowObject,
localFileIndexes: {
urlCol: urlColumnIndex,
statusCol: statusColumnIndex,
sheetName: firstSheetName
}
});
const statusLower = status.toLowerCase();
if (foundWorkingIndex === -1 && statusLower === 'đang tạo') {
foundWorkingIndex = permalinkData.length - 1;
}
if (foundWorkingIndex === -1 && statusLower !== 'đã tạo') {
foundWorkingIndex = permalinkData.length - 1;
}
}
}
}
permalinks = permalinkData;
const newWorksheet = XLSX.utils.aoa_to_sheet(json);
workbookData.Sheets[firstSheetName] = newWorksheet;
if (permalinks.length > 0) {
currentIndex = foundWorkingIndex !== -1 ? foundWorkingIndex : 0;
updateStatus('Đang tạo');
permalinks[currentIndex].status = 'Đang tạo';
processCurrentLink();
} else {
log(`Không tìm thấy URL hoặc tọa độ hợp lệ trong cột ${urlColumnInput}.`, 'warn');
}
updateUIState();
} catch (err) {
log(`Lỗi khi đọc file: ${err.message}`, 'error');
console.error(err);
updateUIState();
}
};
reader.readAsArrayBuffer(file);
}
function saveWorkflows() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(allWorkflows));
log("Đã lưu các workflows.", 'success');
} catch (e) {
log("Lỗi khi lưu workflows vào localStorage.", 'error');
}
}
function loadWorkflows() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
allWorkflows = JSON.parse(saved);
} else {
allWorkflows = { ...defaultWorkflows };
log("Đã tải các workflows mặc định. Các thay đổi sẽ được lưu lại.");
}
} catch (e) {
log("Lỗi khi tải workflows từ localStorage, sử dụng các preset mặc định.", 'error');
allWorkflows = { ...defaultWorkflows };
}
}
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
/**
* Cập nhật status cho URL hiện tại
*/
function updateStatus(status) {
const shouldSave = isStatusSavingEnabled();
if (isGasMode) {
if (permalinks[currentIndex]) {
const item = permalinks[currentIndex];
if (shouldSave) {
updateGasStatusByRowIndex(item.rowIndex, status);
}
item.status = status;
updateSaveButtonState();
}
} else {
if (currentIndex >= 0 && permalinks[currentIndex]) {
const item = permalinks[currentIndex];
if (shouldSave) {
_updateLocalStatusCell(item.rowIndex, item.localFileIndexes.statusCol, status, item.localFileIndexes.sheetName);
}
item.status = status;
hasUnsavedChanges = true;
updateSaveButtonState();
}
}
}
/**
* Tách hàm cập nhật trạng thái ô cụ thể trong workbookData (Local File only)
* @param {number} rowIndex - Row index (0-based)
* @param {number} colIndex - Column index (0-based)
* @param {string} value - New status value
* @param {string} sheetName - Target sheet name
*/
function _updateLocalStatusCell(rowIndex, colIndex, value, sheetName) {
if (!workbookData) return;
try {
const worksheet = workbookData.Sheets[sheetName];
const cellAddress = XLSX.utils.encode_cell({ r: rowIndex, c: colIndex });
worksheet[cellAddress] = { t: 's', v: value };
updateSaveButtonState();
} catch (err) {
log(`Lỗi khi cập nhật cell [${rowIndex}, ${colIndex}] trong file local.`, 'error');
console.error(err);
}
}
/**
* Lưu workbook ra file và trigger download
*/
function saveWorkbookToFile() {
if (isGasMode) {
log("Chế độ Google Sheets: Status được lưu tự động lên sheet.", 'info');
hasUnsavedChanges = false;
updateSaveButtonState();
return;
}
if (!workbookData) {
log('Không có dữ liệu để lưu.', 'warn');
return;
}
try {
const wbout = XLSX.write(workbookData, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([wbout], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = currentFileName;
a.click();
URL.revokeObjectURL(url);
hasUnsavedChanges = false;
updateSaveButtonState();
} catch (err) {
log(`Lỗi khi lưu file: ${err.message}`, 'error');
}
}
/**
* Cập nhật trạng thái nút Save Status
*/
function updateSaveButtonState() {
const saveBtn = document.getElementById('save_status_btn');
if (saveBtn) {
saveBtn.disabled = !hasUnsavedChanges;
if (hasUnsavedChanges) {
saveBtn.classList.add('primary');
saveBtn.style.animation = 'pulse 1.5s infinite';
} else {
saveBtn.classList.remove('primary');
saveBtn.style.animation = '';
}
}
}
function waitForElement(selector, timeout = 7000) {
return new Promise((resolve, reject) => {
const intervalTime = 100;
let elapsedTime = 0;
const interval = setInterval(() => {
const element = document.querySelector(selector);
if (element && element.offsetParent !== null) {
clearInterval(interval);
resolve(element);
}
elapsedTime += intervalTime;
if (elapsedTime >= timeout) {
clearInterval(interval);
reject(new Error(`Element "${selector}" not found or not visible after ${timeout}ms`));
}
}, intervalTime);
});
}
let logQueue = [];
let logTimer = null;
function log(message, type = 'normal') {
const colorMap = {
error: '#c0392b', success: '#27ae60', warn: '#e67e22',
info: '#2980b9', special: '#8e44ad', normal: 'inherit'
};
logQueue.push({
message,
color: colorMap[type],
time: new Date().toLocaleTimeString()
});
if (logTimer) clearTimeout(logTimer);
logTimer = setTimeout(() => {
const logBox = document.getElementById('log_info');
if (!logBox) return;
const fragment = document.createDocumentFragment();
const div = document.createElement('div');
logQueue.forEach(({ message, color, time }) => {
div.innerHTML = `<div style="color:${color}; border-bottom: 1px solid #f0f0f0;">[${time}] ${message}</div>`;
fragment.insertBefore(div.firstChild, fragment.firstChild);
});
logBox.insertBefore(fragment, logBox.firstChild);
while (logBox.children.length > 20) {
logBox.removeChild(logBox.lastChild);
}
logQueue.length = 0;
}, 50);
}
const createElem = (type = "", attrs = {}, eventListeners = []) => {
const el = document.createElement(type || 'div');
Object.keys(attrs).forEach(attr => {
if (attrs[attr] === undefined || attrs[attr] === null) return;
if (['disabled', 'checked', 'selected', 'textContent', 'innerHTML'].includes(attr)) el[attr] = attrs[attr];
else el.setAttribute(attr, attrs[attr]);
});
eventListeners.forEach(obj => {
Object.entries(obj).forEach(([evt, cb]) => el.addEventListener(evt, cb));
});
return el;
};
const createConfigRow = (label, type, id, value, options = []) => {
const container = createElem("div", { style: "margin-bottom: 2px; display: flex; align-items: center; justify-content: flex-start; gap: 5px;" });
let input;
const style = "width: 50%; padding: 1px 2px; border: 1px solid #808080; border-radius: 0; background: #fff; font-size: 11px;";
if (type === "select") {
input = createElem("select", { id, style });
options.forEach(opt => input.appendChild(createElem("option", { value: opt.value, textContent: opt.text, selected: opt.value === value })));
} else {
input = createElem("input", {
id, type: type === "checkbox" ? "checkbox" : (type === "number" ? "number" : "text"),
style: type === "checkbox" ? "margin: 0;" : style,
value: value || ""
});
if (type === "checkbox") input.checked = !!value;
}
container.append(createElem("span", { textContent: label, style: "font-size: 11px; width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" }), input);
return container;
};
async function createUI() {
injectGlobalStyles();
const docFrags = document.createDocumentFragment();
return wmeSDK.Sidebar.registerScriptTab().then(({ tabLabel, tabPane }) => {
tabLabel.innerHTML = `<img src="${GM_info.script.icon}" width="16" height="16" style="margin-top: -2px;">`;
tabLabel.title = "WME Workflow Engine";
docFrags.append(
createElem("div", { style: "font-weight: bold; padding: 5px 0; border-bottom: 1px solid #ccc; margin-bottom: 5px;", textContent: "WME Workflow Engine v" + (GM_info.script.version || "1.0") })
);
const ul = createElem("ul", { class: "nav nav-tabs", style: "margin-bottom: 10px; border-bottom: 1px solid #ddd; display: flex;" });
const tabs = [
{ id: "wfe-data", text: "Dữ liệu", active: true },
{ id: "wfe-configs", text: "Tác vụ" },
{ id: "wfe-settings", text: "Cài đặt" }
];
tabs.forEach(t => {
const li = createElem("li", { class: t.active ? "active" : "", style: "flex: 1; text-align: center;" });
li.append(createElem("a", { "data-toggle": "tab", href: `#panel-${t.id}`, textContent: t.text }));
ul.append(li);
});
docFrags.append(ul);
const contentRoot = createElem("div", { class: "tab-content", style: "max-height: 85vh; overflow-y: auto; overflow-x: hidden; padding: 2px;" });
const p1 = createElem("div", { id: "panel-wfe-data", class: "tab-pane active" });
const sourceToggle = createElem("div", { style: "margin-bottom:10px; font-size:11px" });
sourceToggle.innerHTML = `
<label><input type="radio" name="data_source_mode" value="local" checked> Local</label>
<label style="margin-left:10px"><input type="radio" name="data_source_mode" value="gas"> Google Sheet</label>
`;
p1.append(sourceToggle);
const gasBox = createElem("div", { id: "gas_config", style: "display:none" });
gasBox.append(
createConfigRow("Web App URL:", "text", "gas_url", ""),
createConfigRow("Tên Sheet:", "text", "sheet_name_input", "Sheet1"),
createConfigRow("Header URL:", "text", "url_col_name", "Link WME"),
createElem("button", { id: "load_sheet_btn", class: "action-btn primary", textContent: "Tải dữ liệu từ Google Sheets", style: "width:100%; margin-top:5px" })
);
const localBox = createElem("div", { id: "local_file_config" });
localBox.append(
createElem("input", { type: "file", id: "excel_file", class: "wwe-input", style: "margin-bottom:5px; width: 100%;" }),
createConfigRow("Cột URL (A-Z):", "text", "url_column", "F")
);
p1.append(localBox, gasBox, createElem("button", { id: "save_status_btn", class: "action-btn success", textContent: "💾 Lưu Status", style: "width:100%; margin-top:10px", disabled: true }));
p1.append(gasBox, createElem("hr"), createElem("div", { id: "log_info", class: "log-container" }));
const p2 = createElem("div", { id: "panel-wfe-configs", class: "tab-pane" });
const btnGrp = createElem("div", { style: "display: flex; gap: 5px; margin: 10px 0;" });
btnGrp.append(
createElem("button", { id: "edit_workflow_btn", class: "action-btn", textContent: "Sửa", style: "flex:1" }),
createElem("button", { id: "new_workflow_btn", class: "action-btn", textContent: "Tạo", style: "flex:1" }),
createElem("button", { id: "delete_workflow_btn", class: "action-btn danger", textContent: "Xóa", style: "width:34px" })
);
p2.append(
createConfigRow("Chọn tác vụ:", "select", "workflow_select", ""),
createConfigRow("Giá trị ({{value}}):", "text", "workflow_variable_input", ""),
createElem("div", { class: "wwe-btn-group", style: "margin-top:10px" }),
btnGrp
);
const p3 = createElem("div", { id: "panel-wfe-settings", class: "tab-pane" });
p3.append(
createConfigRow("Zoom level:", "number", "coordinate_zoom", 20),
createConfigRow("Auto Lưu Status", "checkbox", "save_status_enabled", true),
createConfigRow("Auto Lưu Permalink", "checkbox", "save_permalink_after_create", true),
createElem("p", { textContent: "Tự động Lưu (Batch Save):", style: "font-weight:bold; font-size:11px" }),
createConfigRow("Bật Batch Save", "checkbox", "batch_save_enabled", false),
createConfigRow("Số lần run / Save:", "number", "batch_save_limit", 10),
createElem("hr"),
createElem("p", { textContent: "Công cụ POI:", style: "font-weight:bold; font-size:11px" }),
createConfigRow("Loại POI:", "select", "poi_category_select", "CAR_WASH", CATEGORIES.flatMap(c => c.subs.map(s => ({ value: s, text: s })))),
createElem("div", { style: "font-size:11px; margin-top:5px" }).appendChild(
createElem("span", {
innerHTML: `
Tạo: <input type="radio" name="poi_creation_mode" value="none" checked> Ko |
<input type="radio" name="poi_creation_mode" value="point"> Điểm |
<input type="radio" name="poi_creation_mode" value="area"> Vùng
` })
).parentNode,
createElem("div", { style: "font-size:11px; margin-top:5px" }).appendChild(
createElem("span", {
innerHTML: `
Cách tạo: <input type="radio" name="poi_method" value="auto" checked> Tự động |
<input type="radio" name="poi_method" value="manual"> Thủ công (SDK)
` })
).parentNode
);
contentRoot.append(p1, p2, p3);
docFrags.append(contentRoot);
tabPane.appendChild(docFrags);
tabPane.id = "sidepanel-wme-workflow";
const parent = tabPane.parentElement;
if (parent) {
Object.assign(parent.style, { width: "100%", padding: "0 5px", overflow: "hidden", boxSizing: "border-box" });
}
createFloatingHUD();
createWorkflowEditorModal();
populateWorkflowSelector();
});
}
function createFloatingHUD() {
const hud = document.createElement('div');
hud.id = 'wme-wfe-hud';
hud.style.cssText = `
position: fixed; top: 10%; left: 80%;
background: rgba(255, 255, 255, 0.9);
z-index: 1001; width: 100px; overflow: hidden;
opacity: 0.8;
`;
hud.innerHTML = `
<div id="wfe-hud-header" style="background: #c0c0c0; border: 2px outset #ffffff; border-bottom: none; padding: 2px 4px; cursor: move; display: flex; align-items: center; justify-content: flex-start; gap: 4px;">
<span style="font-weight:bold; font-size: 10px; color: #000;">Workflow Engine</span>
</div>
<div style="padding: 4px; background: #c0c0c0; border: 2px outset #ffffff;">
<!-- Navigation -->
<div style="display: flex; justify-content: flex-start; align-items: center; gap: 4px;">
<button id="prev_btn" class="nav-btn" title="Lùi" disabled>◀</button>
<input type="number" id="nav_index_input" min="1" style="width: 30px; text-align: center; border: 1px inset #808080; background: #fff; font-size: 10px; font-weight: bold; margin: 0;" disabled>
<div id="nav_total_count" style="font-size: 9px; color: #000;">/ 0</div>
<button id="next_btn" class="nav-btn" title="Tiếp" disabled>▶</button>
</div>
<!-- Actions -->
<button id="reselect_btn" class="hud-btn secondary" title="Reload" disabled>Reload</button>
<button id="run_workflow_btn" class="hud-btn primary" title="Run" disabled>Run</button>
<button id="loop_workflow_btn" class="hud-btn warning" title="Loop" disabled>Loop</button>
</div>
`;
document.body.appendChild(hud);
makeDraggable(hud, document.getElementById('wfe-hud-header'));
bindHudEvents();
}
function injectGlobalStyles() {
if (document.getElementById('wme-wfe-styles')) return;
const style = document.createElement('style');
style.id = 'wme-wfe-styles';
style.innerHTML = `
#workflow-editor-modal {
display: none; position: fixed; z-index: 20000;
left: 0; top: 0; width: 100vw; height: 100vh;
background: rgba(0,0,0,0.5); overflow: hidden;
}
#workflow-editor-content {
position: absolute; left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: 600px; max-height: 90vh;
background: #c0c0c0; border: 2px outset #ffffff;
display: flex; flex-direction: column;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
}
/* General */
.wwe-input { border-radius: 0; border: 1px inset #808080; background: #fff; width: 100%; padding: 2px; box-sizing: border-box; font-size: 11px; }
.wwe-form-group { margin-bottom: 4px; }
.wwe-form-group label { font-weight: bold; font-size: 11px; display: block; margin-bottom: 1px; }
.w-100 { width: 100%; }
.mt-2 { margin-top: 4px; }
/* Buttons Class98 */
.action-btn, .hud-btn, .nav-btn {
cursor: pointer; border: 2px outset #ffffff; border-radius: 0;
background: #c0c0c0; transition: none; font-family: 'Tahoma', sans-serif;
font-size: 11px; padding: 2px 4px; color: #000;
}
.action-btn:active, .hud-btn:active, .nav-btn:active {
border: 2px inset #ffffff;
}
.action-btn:disabled, .hud-btn:disabled, .nav-btn:disabled { opacity: 0.6; cursor: default; }
.primary { font-weight: bold; }
.success { font-weight: bold; }
.danger { color: #800000; }
.warning { color: #000; }
.secondary { color: #000; }
#loop_workflow_btn.looping { background-color: #ff5722 !important; color: #fff; animation: pulse 1s infinite; }
/* Accordion */
.accordion-header {
width: 100%; text-align: left; padding: 2px 5px; background: #c0c0c0; border: 1px outset #fff;
font-weight: bold; font-size: 11px; cursor: pointer;
display: flex; justify-content: flex-start; align-items: center; gap: 5px;
}
.accordion-header::before { content: '[+]'; font-family: monospace; }
.accordion-header.active::before { content: '[-]'; }
.accordion-content { max-height: 0; overflow: hidden; transition: none; padding: 0 5px; background: #c0c0c0; }
/* Utils */
.log-container { height: 80px; overflow-y: auto; font-size: 10px; background: #fff; border: 1px inset #808080; padding: 2px; }
.wwe-btn-group { display: flex; gap: 2px; margin-top: 2px; }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
// /* Nav Tabs Class98 */
// .nav-tabs { border-bottom: 1px solid #808080 !important; }
// .nav-tabs > li > a { border-radius: 0 !important; margin-right: 2px; padding: 2px 8px !important; background: #d0d0d0; border: 1px solid #808080; color: #000; }
// .nav-tabs > li.active > a { background: #c0c0c0 !important; border-bottom-color: transparent !important; font-weight: bold; }
`;
document.head.appendChild(style);
}
function bindSidebarEvents() {
const contentPanel = document.getElementById('wwe-sidebar-content');
if (contentPanel) {
contentPanel.addEventListener('click', (e) => {
if (e.target.classList.contains('accordion-header')) {
const content = e.target.nextElementSibling;
e.target.classList.toggle('active');
content.style.maxHeight = content.style.maxHeight ? null : (content.scrollHeight + 10 + "px");
}
});
}
const toggleSource = (e) => {
if (e.target.name !== 'data_source_mode') return;
const isLocal = e.target.value === 'local';
document.getElementById('local_file_config').style.display = isLocal ? 'block' : 'none';
document.getElementById('gas_config').style.display = isLocal ? 'none' : 'block';
document.getElementById('save_status_btn').textContent = isLocal ? '💾 Lưu Status' : '☁️ Auto Sync';
};
document.querySelectorAll('input[name="data_source_mode"]').forEach(el => el.addEventListener('change', toggleSource));
document.getElementById('excel_file').addEventListener('change', handleFile, false);
registerEventCleanup(document.getElementById('load_sheet_btn'), 'click', loadFromGoogleSheet);
registerEventCleanup(document.getElementById('save_status_btn'), 'click', saveWorkbookToFile);
registerEventCleanup(document.getElementById('poi_category_select'), 'change', (e) => selectedSubCategory = e.target.value);
registerEventCleanup(document.getElementById('edit_workflow_btn'), 'click', () => {
const id = document.getElementById('workflow_select').value;
if (id) openWorkflowEditor(id);
});
registerEventCleanup(document.getElementById('new_workflow_btn'), 'click', () => openWorkflowEditor());
registerEventCleanup(document.getElementById('delete_workflow_btn'), 'click', deleteSelectedWorkflow);
registerEventCleanup(document.getElementById('workflow_select'), 'change', () => {
updateUIState();
if (currentIndex >= 0 && permalinks.length > 0) processCurrentLink();
});
const sidebar = document.getElementById('sidepanel-wme-workflow');
if (sidebar) {
sidebar.addEventListener('change', (e) => {
if (SETTINGS_IDS.includes(e.target.id) || ['data_source_mode', 'poi_creation_mode'].includes(e.target.name)) {
saveAllSettings();
}
});
}
updateUIState();
}
function bindHudEvents() {
const bindings = [
{ id: 'prev_btn', evt: 'click', fn: () => navigate(-1) },
{ id: 'next_btn', evt: 'click', fn: () => navigate(1) },
{ id: 'reselect_btn', evt: 'click', fn: processCurrentLink },
{ id: 'run_workflow_btn', evt: 'click', fn: () => runSelectedWorkflow(false) },
{ id: 'loop_workflow_btn', evt: 'click', fn: toggleWorkflowLoop },
{ id: 'nav_index_input', evt: 'change', fn: (e) => navigate(0, parseInt(e.target.value) - 1) }
];
bindings.forEach(b => {
const el = document.getElementById(b.id);
if (el) el.addEventListener(b.evt, b.fn);
});
}
/**
* Makes an element draggable using its handle.
* @param {HTMLElement} elementToMove The element that will be moved.
* @param {HTMLElement} dragHandle The element that acts as the drag handle.
*/
function makeDraggable(elementToMove, dragHandle) {
let offsetX, offsetY;
let isDragging = false;
dragHandle.onmousedown = (e) => {
e.preventDefault();
isDragging = true;
dragHandle.style.cursor = 'grabbing';
const computedStyle = getComputedStyle(elementToMove);
if (computedStyle.position === 'static') {
elementToMove.style.position = 'absolute';
}
if (computedStyle.transform && computedStyle.transform !== 'none') {
const matrix = new DOMMatrixReadOnly(computedStyle.transform);
elementToMove.style.left = (elementToMove.offsetLeft + matrix.m41) + 'px';
elementToMove.style.top = (elementToMove.offsetTop + matrix.m42) + 'px';
elementToMove.style.transform = 'none';
}
const rect = elementToMove.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
document.onmousemove = (ev) => {
if (!isDragging) return;
elementToMove.style.left = (ev.clientX - offsetX) + 'px';
elementToMove.style.top = (ev.clientY - offsetY) + 'px';
};
document.onmouseup = () => {
isDragging = false;
document.onmouseup = null;
document.onmousemove = null;
dragHandle.style.cursor = 'grab';
};
};
}
function isStatusSavingEnabled() {
return document.getElementById('save_status_enabled')?.checked === true;
}
function createWorkflowEditorModal() {
let modal = document.getElementById('workflow-editor-modal');
if (modal) return;
modal = document.createElement('div');
modal.id = 'workflow-editor-modal';
modal.innerHTML = `
<div id="workflow-editor-content">
<div id="editor-header" style="background:#000080; color:#fff; padding:2px 8px; cursor:move; display:flex; justify-content:flex-start; align-items:center; gap:8px;">
<strong id="editor-title" style="flex:1; font-size:11px;">Cấu hình Workflow</strong>
<span id="close-modal" style="cursor:pointer; font-size:16px; font-weight:bold;">[X]</span>
</div>
<div id="editor-panel-content" style="padding:8px;">
<input type="hidden" id="editing_workflow_id">
<div class="wwe-form-group">
<label>Tên tác vụ:</label>
<input type="text" id="workflow_name_input" class="wwe-input" placeholder="Ví dụ: Update CHXD">
</div>
<hr style="border:inset 1px #fff; margin:8px 0;">
<div id="sdk-tasks-container" style="max-height: 400px; overflow-y: auto;"></div>
</div>
<div style="padding:8px; border-top:1px inset #fff; display:flex; justify-content:flex-start; gap:8px; background: #c0c0c0;">
<button id="cancel_workflow_btn" class="action-btn">Hủy</button>
<button id="save_workflow_btn" class="action-btn primary">Lưu</button>
</div>
</div>`;
document.body.appendChild(modal);
document.getElementById('close-modal').onclick = closeWorkflowEditor;
document.getElementById('cancel_workflow_btn').onclick = closeWorkflowEditor;
document.getElementById('save_workflow_btn').onclick = saveWorkflowFromEditor;
makeDraggable(document.getElementById('workflow-editor-content'), document.getElementById('editor-header'));
}
function renderSdkTasksInEditor(existingTasks = []) {
const container = document.getElementById('sdk-tasks-container');
container.innerHTML = '';
Object.keys(SDK_REGISTRY).forEach(taskId => {
const def = SDK_REGISTRY[taskId];
const existing = existingTasks.find(t => t.taskId === taskId) || { enabled: false, params: {} };
const wrapper = document.createElement('div');
wrapper.style.cssText = "border: 1px solid #ddd; margin-bottom: 8px; padding: 10px; border-radius: 4px; background: #fafafa;";
const header = document.createElement('div');
header.innerHTML = `
<label style="font-weight: bold; color: #333; display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" class="task-enable-cb" data-task-id="${taskId}" ${existing.enabled ? 'checked' : ''} style="width: auto; margin-right: 8px;">
${def.name}
</label>
<div style="font-size: 0.85em; color: #666; margin-left: 24px; margin-bottom: 5px;">${def.description}</div>
`;
wrapper.appendChild(header);
if (def.params.length > 0) {
const paramsDiv = document.createElement('div');
paramsDiv.className = 'task-params';
paramsDiv.style.cssText = `margin-left: 24px; display: ${existing.enabled ? 'block' : 'none'};`;
def.params.forEach(p => {
const row = document.createElement('div');
row.style.marginBottom = '5px';
const val = existing.params[p.key] || '';
row.innerHTML = `
<label style="display:block; font-size: 11px; margin-bottom: 2px;">${p.label}:</label>
<input type="text" class="task-param-input" data-task-id="${taskId}" data-param-key="${p.key}" value="${val}" placeholder="${p.placeholder}" style="width: 100%;">
`;
paramsDiv.appendChild(row);
});
wrapper.appendChild(paramsDiv);
}
container.appendChild(wrapper);
});
container.querySelectorAll('.task-enable-cb').forEach(cb => {
cb.addEventListener('change', (e) => {
const paramsDiv = e.target.closest('div').parentElement.querySelector('.task-params');
if (paramsDiv) paramsDiv.style.display = e.target.checked ? 'block' : 'none';
});
});
}
function registerHotkeys() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
const actions = {
'ArrowRight': 'next_btn',
'ArrowLeft': 'prev_btn',
'ArrowUp': 'reselect_btn',
'ArrowDown': 'run_workflow_btn'
};
const btnId = actions[e.key];
if (btnId) {
const btn = document.getElementById(btnId);
if (btn && !btn.disabled) {
e.preventDefault();
btn.click();
}
}
if (e.key.toLowerCase() === 'u') {
e.preventDefault();
updateStatus('Không tồn tại');
WazeToastr.Alerts.info('WME Workflow Engine','Đã đánh dấu: Không tồn tại', false, false, 2000);
}
});
}
function navigate(direction, targetIndex = null) {
if (isLooping || permalinks.length === 0) return;
let newIndex = (targetIndex !== null) ? targetIndex : (currentIndex + direction);
if (newIndex < 0 || newIndex >= permalinks.length) return;
placeholderCache.clear();
const oldIndex = currentIndex;
currentIndex = newIndex;
if (direction > 0 && oldIndex >= 0) {
const oldItem = permalinks[oldIndex];
if (!oldItem.status || oldItem.status.toLowerCase() === 'đang tạo') {
updateStatusByIndex(oldIndex, 'Đã tạo');
}
}
if (permalinks[currentIndex].status.toLowerCase() !== 'đã tạo') {
updateStatus('Đang tạo');
}
processCurrentLink();
updateUIState();
}
/**
* Cập nhật status cho một URL cụ thể theo index
*/
function updateStatusByIndex(index, status) {
const shouldSave = isStatusSavingEnabled();
if (isGasMode) {
if (index >= 0 && permalinks[index]) {
const item = permalinks[index];
if (shouldSave) {
updateGasStatusByRowIndex(item.rowIndex, status);
}
item.status = status;
}
} else {
if (index >= 0 && permalinks[index] && permalinks[index].localFileIndexes) {
const item = permalinks[index];
if (shouldSave) {
_updateLocalStatusCell(item.rowIndex, item.localFileIndexes.statusCol, status, item.localFileIndexes.sheetName);
}
item.status = status;
}
}
}
function extractCoords(item) {
const coordMatch = item.url.match(/(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)/);
if (coordMatch) return { lat: parseFloat(coordMatch[1]), lon: parseFloat(coordMatch[2]) };
const urlMatch = item.url.match(/lat=(-?\d+\.\d+)&lon=(-?\d+\.\d+)/);
if (urlMatch) return { lat: parseFloat(urlMatch[1]), lon: parseFloat(urlMatch[2]) };
return null;
}
async function handleBatchSave() {
const isEnabled = document.getElementById('batch_save_enabled')?.checked;
if (!isEnabled) return;
const limit = parseInt(document.getElementById('batch_save_limit')?.value) || 1;
batchRunCounter++;
if (batchRunCounter >= limit) {
try {
await wmeSDK.Editing.save();
batchRunCounter = 0;
log(`Batch Save: Đã tự động lưu ${limit} đối tượng.`, 'success');
} catch (err) {
log(`Lỗi khi Batch Save: ${err.message}`, 'error');
}
}
}
async function processCurrentLink() {
if (currentIndex < 0 || currentIndex >= permalinks.length) return;
const item = permalinks[currentIndex];
currentRowData = item.rowData;
const coords = extractCoords(item);
if (coords) {
const zoom = parseInt(document.getElementById('coordinate_zoom')?.value || 20);
W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(coords.lon, coords.lat), zoom);
}
parseWazeUrlAndNavigate(item.url, true);
}
function updatePermalinkInWorkbook(index, newPermalink) {
if (!document.getElementById('save_permalink_after_create')?.checked) return;
const item = permalinks[index];
if (!item) return;
if (isGasMode) {
updateGasStatusByRowIndex(item.rowIndex, item.status, newPermalink);
item.url = newPermalink;
const urlColName = document.getElementById('url_col_name')?.value?.trim() || 'Link WME';
item.rowData[urlColName] = newPermalink;
} else {
if (!workbookData || !item.localFileIndexes) return;
try {
const sheet = workbookData.Sheets[item.localFileIndexes.sheetName];
const statusColIndex = item.localFileIndexes.statusCol;
const newColIndex = statusColIndex + 1;
const NEW_COL_NAME = "New Permalink";
const rowIndex = item.rowIndex;
const headerAddress = XLSX.utils.encode_cell({ r: 0, c: newColIndex });
if (!sheet[headerAddress] || sheet[headerAddress].v !== NEW_COL_NAME) {
sheet[headerAddress] = { t: 's', v: NEW_COL_NAME };
}
const cellAddress = XLSX.utils.encode_cell({ r: rowIndex, c: newColIndex });
sheet[cellAddress] = { t: 's', v: newPermalink };
const range = XLSX.utils.decode_range(sheet['!ref']);
if (newColIndex > range.e.c) {
range.e.c = newColIndex;
sheet['!ref'] = XLSX.utils.encode_range(range);
}
item.url = newPermalink;
const urlColLetter = getColumnLetter(item.localFileIndexes.urlCol);
item.rowData[urlColLetter] = newPermalink;
hasUnsavedChanges = true;
updateSaveButtonState();
} catch (err) {
log(`❌ Lỗi khi cập nhật file local: ${err.message}`, "error");
console.error(err);
}
}
}
async function parseWazeUrlAndNavigate(value, onlyPan = false) {
try {
const trimmedValue = value.trim();
const coordMatch = trimmedValue.match(/^\s*\(?\s*(-?\d+\.?\d*)\s*,\s*(-?\d+\.?\d*)\s*\)?\s*$/);
if (coordMatch) {
const lat = parseFloat(coordMatch[1]);
const lon = parseFloat(coordMatch[2]);
if (isNaN(lat) || isNaN(lon)) {
throw new Error('Tọa độ không hợp lệ.');
}
const zoomInput = document.getElementById('coordinate_zoom');
const defaultZoom = zoomInput ? parseInt(zoomInput.value, 10) : 20;
W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(lon, lat), defaultZoom);
W.selectionManager.setSelectedModels([]);
return;
}
const parsedUrl = new URL(trimmedValue);
const params = parsedUrl.searchParams;
const lon = parseFloat(params.get('lon'));
const lat = parseFloat(params.get('lat'));
const zoom = parseInt(params.get('zoomLevel') || params.get('zoom'), 10) + 2;
W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(lon, lat), zoom);
if (onlyPan) return;
const segmentIDs = (params.get('segments') || '').split(',').filter(id => id);
const venueIDs = (params.get('venues') || '').split(',').filter(id => id);
WazeWrap.Model.onModelReady(() => {
(async () => {
await delay(1000);
let objectsToSelect = [];
if (segmentIDs.length > 0) {
const segments = segmentIDs.map(id => W.model.segments.getObjectById(id)).filter(Boolean);
if (segments.length === 0) {
log(`Cảnh báo: Không tìm thấy segment nào từ ID ${segmentIDs.join(',')} sau khi tải.`, 'warn');
} else {
objectsToSelect.push(...segments);
}
}
if (venueIDs.length > 0) {
const venues = venueIDs.map(id => W.model.venues.getObjectById(id)).filter(Boolean);
if (venues.length === 0) {
log(`Cảnh báo: Không tìm thấy venue nào từ ID ${venueIDs.join(',')} sau khi tải.`, 'warn');
} else {
objectsToSelect.push(...venues);
}
}
if (objectsToSelect.length > 0) {
W.selectionManager.setSelectedModels(objectsToSelect);
}
})();
}, true);
} catch (error) {
log(`Lỗi khi xử lý "${value}": ${error.message}`, 'error');
console.error(error);
}
}
function updateUIState() {
const hasLinks = permalinks.length > 0;
const isEnd = currentIndex >= permalinks.length - 1;
const isStart = currentIndex <= 0;
const elements = {
'prev_btn': !hasLinks || isStart || isLooping,
'next_btn': !hasLinks || isEnd || isLooping,
'reselect_btn': !hasLinks || isLooping,
'run_workflow_btn': !hasLinks || !document.getElementById('workflow_select').value || isLooping,
'nav_index_input': !hasLinks || isLooping,
'loop_workflow_btn': !hasLinks
};
Object.entries(elements).forEach(([id, disabled]) => {
const el = document.getElementById(id);
if (el) el.disabled = disabled;
});
const navInput = document.getElementById('nav_index_input');
const navTotal = document.getElementById('nav_total_count');
if (navInput && hasLinks) {
navInput.value = currentIndex + 1;
navInput.max = permalinks.length;
navTotal.textContent = ` / ${permalinks.length}`;
}
const loopBtn = document.getElementById('loop_workflow_btn');
if (loopBtn) {
loopBtn.textContent = isLooping ? 'Dừng Lặp' : 'Bắt đầu Lặp';
loopBtn.classList.toggle('looping', isLooping);
}
updateSaveButtonState();
}
function populateWorkflowSelector() {
const select = document.getElementById('workflow_select');
if (!select) return;
const currentId = select.value;
select.innerHTML = '';
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = Object.keys(allWorkflows).length === 0 ? '--- Không có workflow ---' : '--- Chọn workflow ---';
select.appendChild(emptyOption);
for (const id in allWorkflows) {
const option = document.createElement('option');
option.value = id;
option.textContent = allWorkflows[id].name;
select.appendChild(option);
}
if (currentId && allWorkflows[currentId]) {
select.value = currentId;
} else if (Object.keys(allWorkflows).length > 0) {
const firstWorkflowId = Object.keys(allWorkflows)[0];
if (firstWorkflowId) {
select.value = firstWorkflowId;
}
} else {
select.value = '';
}
updateUIState();
}
function deleteSelectedWorkflow() {
const select = document.getElementById('workflow_select');
const idToDelete = select.value;
const workflowName = allWorkflows[idToDelete]?.name;
if (!idToDelete) {
alert("Vui lòng chọn một workflow để xóa.");
return;
}
if (confirm(`Bạn có chắc chắn muốn xóa workflow "${workflowName}" không?`)) {
delete allWorkflows[idToDelete];
saveWorkflows();
populateWorkflowSelector();
log(`Đã xóa workflow: "${workflowName}"`, 'info');
}
}
function openWorkflowEditor(workflowId = null) {
const modal = document.getElementById('workflow-editor-modal');
const nameInput = document.getElementById('workflow_name_input');
const idInput = document.getElementById('editing_workflow_id');
const title = document.getElementById('editor-title');
if (workflowId && allWorkflows[workflowId]) {
const wf = allWorkflows[workflowId];
title.textContent = "Chỉnh sửa Workflow (SDK)";
nameInput.value = wf.name;
idInput.value = workflowId;
renderSdkTasksInEditor(wf.tasks || []);
} else {
title.textContent = "Tạo Workflow Mới (SDK)";
nameInput.value = '';
idInput.value = '';
renderSdkTasksInEditor([]);
}
modal.style.display = 'block';
}
function closeWorkflowEditor() {
document.getElementById('workflow-editor-modal').style.display = 'none';
}
function saveWorkflowFromEditor() {
const name = document.getElementById('workflow_name_input').value.trim();
if (!name) return alert("Vui lòng nhập tên tác vụ.");
const tasks = [];
document.querySelectorAll('.task-enable-cb').forEach(cb => {
if (cb.checked) {
const taskId = cb.dataset.taskId;
const params = {};
document.querySelectorAll(`.task-param-input[data-task-id="${taskId}"]`).forEach(inp => {
params[inp.dataset.paramKey] = inp.value;
});
tasks.push({ taskId, enabled: true, params });
}
});
if (tasks.length === 0) return alert("Vui lòng chọn ít nhất một hành động.");
let id = document.getElementById('editing_workflow_id').value;
if (!id) id = `sdk_wf_${Date.now()}`;
allWorkflows[id] = { name, tasks };
saveWorkflows();
populateWorkflowSelector();
document.getElementById('workflow_select').value = id;
closeWorkflowEditor();
log(`Đã lưu workflow SDK "${name}"`, 'success');
}
function saveAllSettings() {
const settings = {};
SETTINGS_IDS.forEach(id => {
const el = document.getElementById(id);
if (el) {
if (el.type === 'checkbox') settings[id] = el.checked;
else settings[id] = el.value;
}
});
const dataSource = document.querySelector('input[name="data_source_mode"]:checked')?.value;
if (dataSource) settings.data_source_mode = dataSource;
const poiMode = document.querySelector('input[name="poi_creation_mode"]:checked')?.value;
if (poiMode) settings.poi_creation_mode = poiMode;
localStorage.setItem(STORAGE_KEY_SETTINGS, JSON.stringify(settings));
}
function loadAllSettings() {
try {
const saved = localStorage.getItem(STORAGE_KEY_SETTINGS);
if (!saved) return;
const settings = JSON.parse(saved);
Object.entries(settings).forEach(([id, val]) => {
const el = document.getElementById(id);
if (el) {
if (el.type === 'checkbox') el.checked = val;
else el.value = val;
}
});
if (settings.data_source_mode) {
const r = document.querySelector(`input[name="data_source_mode"][value="${settings.data_source_mode}"]`);
if (r) {
r.checked = true;
const evt = new Event('change', { bubbles: true });
Object.defineProperty(evt, 'target', { value: r, enumerable: true });
document.getElementById('local_file_config').style.display = settings.data_source_mode === 'local' ? 'block' : 'none';
document.getElementById('gas_config').style.display = settings.data_source_mode === 'local' ? 'none' : 'block';
document.getElementById('save_status_btn').textContent = settings.data_source_mode === 'local' ? '💾 Lưu Status' : '☁️ Auto Sync';
}
}
if (settings.poi_creation_mode) {
const r = document.querySelector(`input[name="poi_creation_mode"][value="${settings.poi_creation_mode}"]`);
if (r) r.checked = true;
}
if (settings.poi_category_select) selectedSubCategory = settings.poi_category_select;
} catch (e) {
log('Lỗi khi tải cài đặt.', 'error');
console.error(e);
}
}
resetData()
/**
* Tải dữ liệu từ Google Sheets thông qua GAS Web App.
*/
function loadFromGoogleSheet() {
const scriptUrl = document.getElementById('gas_url')?.value?.trim() || '';
const sheetName = document.getElementById('sheet_name_input')?.value?.trim() || '';
const urlColName = document.getElementById('url_col_name')?.value?.trim() || '';
const skipDone = document.getElementById('skip_done_check')?.checked || false;
if (!scriptUrl) { alert("Vui lòng nhập Web App URL!"); return; }
saveAllSettings();
permalinks = [];
currentIndex = -1;
previousIndex = -1;
isGasMode = true;
gasHeaders = null;
hasUnsavedChanges = false;
log("Đang tải dữ liệu từ Google Sheets...");
const loadBtn = document.getElementById('load_sheet_btn');
if (loadBtn) loadBtn.disabled = true;
const readUrl = `${scriptUrl}?action=get&sheetName=${encodeURIComponent(sheetName)}`;
GM_xmlhttpRequest({
method: "GET",
url: readUrl,
onload: function (response) {
if (loadBtn) loadBtn.disabled = false;
if (response.status !== 200) {
log(`❌ Lỗi kết nối GAS: Status ${response.status}. Kiểm tra URL và quyền truy cập.`, 'error');
isGasMode = false;
updateUIState();
return;
}
try {
const json = JSON.parse(response.responseText);
if (json.result === "success" && json.data) {
if (json.headers && Array.isArray(json.headers)) {
gasHeaders = json.headers;
log("Đã load dữ liệu từ GG Sheets thành công", 'success')
} else {
log("Cảnh báo: Không nhận được headers từ GAS, mapping {{tên cột}} có thể không chính xác.", 'warn');
}
let foundIndex = -1;
let tempPermalinks = [];
json.data.forEach(row => {
const url = row[urlColName] || "";
const stt = row[STATUS_COL_NAME] || "";
const rowIndex = row["_rowIndex"] || null;
if (url && rowIndex !== null) {
const statusTrimmed = stt.toString().trim().toLowerCase();
if (skipDone && statusTrimmed === 'đã tạo') {
return;
}
tempPermalinks.push({
url: url.toString().trim(),
rowIndex: rowIndex,
status: statusTrimmed,
rowData: row
});
if (foundIndex === -1 && statusTrimmed === 'đang tạo') {
foundIndex = tempPermalinks.length - 1;
}
}
});
permalinks = tempPermalinks;
if (foundIndex === -1) {
foundIndex = permalinks.findIndex(p => p.status !== 'đã tạo');
if (foundIndex === -1 && permalinks.length > 0) foundIndex = 0;
}
currentIndex = foundIndex === -1 ? 0 : foundIndex;
if (permalinks.length > 0) {
updateStatus('Đang tạo');
}
updateUIState();
processCurrentLink();
} else {
log("❌ Lỗi Sheet: " + (json.message || "Lỗi dữ liệu trả về."));
isGasMode = false;
}
} catch (e) {
log("❌ Lỗi parse JSON hoặc lỗi xử lý dữ liệu: " + e.message, 'error');
console.error(e);
isGasMode = false;
}
},
onerror: function (err) {
if (loadBtn) loadBtn.disabled = false;
log("❌ Lỗi kết nối mạng GAS.", 'error');
console.error(err);
isGasMode = false;
}
});
}
function updateGasStatusByRowIndex(rowIndex, newStatus, newPermalink = null) {
if (!isGasMode || !rowIndex) return;
const scriptUrl = document.getElementById('gas_url')?.value?.trim();
const sheetName = document.getElementById('sheet_name_input')?.value?.trim();
if (!scriptUrl || !sheetName) {
log('Lỗi: Thiếu Web App URL hoặc Tên Sheet để cập nhật GAS.', 'error');
return;
}
let url = `${scriptUrl}?action=post&rowIndex=${rowIndex}&status=${encodeURIComponent(newStatus)}&sheetName=${encodeURIComponent(sheetName)}`;
if (newPermalink) {
const urlColName = document.getElementById('url_col_name')?.value?.trim();
if (urlColName) {
url += `&urlCol=${encodeURIComponent(urlColName)}`;
url += `&permalink=${encodeURIComponent(newPermalink)}`;
}
}
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: (response) => {
try {
const res = JSON.parse(response.responseText);
if (res.result !== "success") {
log(`⚠️ Lỗi GAS ghi status (Row ${rowIndex}): ${res.message}`, 'warn');
}
} catch (e) {
log(`⚠️ Lỗi phản hồi JSON từ GAS khi ghi status.`, 'warn');
}
},
onerror: (err) => {
log(`❌ Lỗi kết nối khi cập nhật GAS status (Row ${rowIndex}).`, 'error');
}
});
}
bootstrap();
})();