WME Workflow Engine

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.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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();
})();