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.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         WME Workflow Engine
// @namespace    https://greasyfork.org/
// @version      2.1.8
// @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
// @connect      *
// @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
// @icon         data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iNTBtbSIKICAgaGVpZ2h0PSI1MG1tIgogICB2aWV3Qm94PSIwIDAgNTAgNTAiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzEiCiAgIGlua3NjYXBlOnZlcnNpb249IjEuNC4yIChmNDMyN2Y0LCAyMDI1LTA1LTEzKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0id21lLXdmLWUuc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzEiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjMDAwMDAwIgogICAgIGJvcmRlcm9wYWNpdHk9IjAuMjUiCiAgICAgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIwIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iCiAgICAgaW5rc2NhcGU6em9vbT0iMy4wNDY3MTcyIgogICAgIGlua3NjYXBlOmN4PSI4Ni40ODY1MzEiCiAgICAgaW5rc2NhcGU6Y3k9IjEzNi4zNzYzIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMjU2MCIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDU3IgogICAgIGlua3NjYXBlOndpbmRvdy14PSItOCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTgiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiIC8+CiAgPGRlZnMKICAgICBpZD0iZGVmczEiPgogICAgPHBhdHRlcm4KICAgICAgIGlua3NjYXBlOmNvbGxlY3Q9ImFsd2F5cyIKICAgICAgIHhsaW5rOmhyZWY9IiNhcDEwOCIKICAgICAgIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiCiAgICAgICBpZD0icGF0dGVybjE4IgogICAgICAgcGF0dGVyblRyYW5zZm9ybT0ibWF0cml4KDAuMDUsMCwwLDAuMDUsMTE4MC4wMDM0LDU0My4yNjE4NSkiCiAgICAgICB4PSI5NyIKICAgICAgIHk9IjAiIC8+CiAgICA8cGF0dGVybgogICAgICAgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSIKICAgICAgIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIgogICAgICAgd2lkdGg9IjM3Ny44NzUzNSIKICAgICAgIGhlaWdodD0iMTk1Ljk0ODkzIgogICAgICAgcGF0dGVyblRyYW5zZm9ybT0idHJhbnNsYXRlKDExODAuMDAzNCw1NDMuMjYxODUpIHNjYWxlKDAuMikiCiAgICAgICBzdHlsZT0iZmlsbDojZjYwMDAwIgogICAgICAgaWQ9ImFwMTA4IgogICAgICAgaW5rc2NhcGU6Y29sbGVjdD0iYWx3YXlzIgogICAgICAgaW5rc2NhcGU6aXNzdG9jaz0idHJ1ZSIKICAgICAgIGlua3NjYXBlOmxhYmVsPSJhcDEwOCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTkzODYiCiAgICAgICAgIHN0eWxlPSJvcGFjaXR5OjE7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjkuMDA0OTE7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1vcGFjaXR5OjE7cGFpbnQtb3JkZXI6bWFya2VycyBmaWxsIHN0cm9rZSIKICAgICAgICAgZD0ibSAwLDAgdiAyLjIxNDA0NzIgYyA0LjQ1OTA0ODgsMCA3Ljk5MTU4NDMsMy41MzI0MjIxIDcuOTkxNTg0Myw3Ljk5MTQ3MDggMCwyLjMzNDgwMyAtMC45NzYxMzg2LDQuNDA2MjQ5IC0yLjUzOTAxMTEsNS44NTQ1MjYgQyAzLjY0ODExMzQsMTUuOTU0NTk1IDEuODMwNTc2NCwxNS45MDA1NDggMCwxNS45MDA1NDggdiA4LjUxNDc0NiBDIDM2LjExNzU4MSwyNC40MTkwNzQgNjYuODMzODc3LDQ3LjI5MTM3NiA3OC41MjcyMDYsNzkuMzA5MzggNTQuMzA2NTU3LDgzLjUwMzMzMiAzMy4yODM3NjcsOTcuMDc1MjM4IDE5LjMwOTY4MiwxMTYuMTM4NTcgMTMuMDgxMTM0LDExNC44MDY3OCA2LjYyMjI2MTQsMTE0LjEwMDggMCwxMTQuMTAwOCB2IDguNTE0NjMgYyAzNi4wNTY3MzEsMCA2Ni43Mjk5MDIsMjIuNzUzOTMgNzguNDY2NTQ1LDU0LjcwNjgxIC0zLjQ3NzMxNywwLjYwNDYxIC02Ljg4NzY5OCwxLjM5OTM3IC0xMC4yMjE5OTcsMi4zNzkzNyBDIDU3LjYwOTI5OCwxNTIuMzg2NTggMzEuMDM4MTYxLDEzMi45ODA3MSAwLDEzMi45ODA3MSB2IDguNTMxMjYgYyAyNy4zOTMxODQsMCA1MC43NTU4OCwxNi45NTk4IDYwLjE5NzkzNCw0MC45NjUzNiAtMy4yNDQ3MjUsMS4yODg3NCAtNi40MDIxMDQsMi43NTI4MiAtOS40NTY1MjksNC4zODQxIEMgNDIuOTMxMzUxLDE2Ni40MzUxNiAyMy4xMzQyOTksMTUxLjg4Mjg5IDAsMTUxLjg4Mjc3IHYgOC41MzEyNiBjIDIwLjExMzA1OCwxLjJlLTQgMzcuMTQyMjQ5LDEyLjg4NjggNDMuMzA2MTI5LDMwLjg3NTQ5IC0yLjIwMTk1MywxLjQ1MzU0IC00LjMxNTk1NiwzLjAyOTgyIC02LjM4MzI0NCw0LjY1OTQxIEggNTIuMTAxNzcgYyAxMi40NTAzNjksLTcuMjI3MjkgMjYuOTE1MzM5LC0xMS4zNzg2MSA0Mi4zNjk4NjUsLTExLjM3ODYxIDE1LjQ0NjU4NSwwIDI5Ljg5NzI3NSw0LjE1NzAyIDQyLjM0MjM0NSwxMS4zNzg2MSBoIDE1LjIyODQ0IGMgLTExLjg2ODU1LC05LjM2ODc3IC0yNi4wNTA1MSwtMTUuOTI5MiAtNDEuNTYwMTgsLTE4LjYyNjY5IDExLjc0OTUsLTMxLjkxNDg2IDQyLjQwOTgyLC01NC43MDEyNSA3OC40NTU0NCwtNTQuNzAxMjUgMzYuMDM4NzcsMCA2Ni42OTUxNiwyMi43Nzk4NSA3OC40NTAwMiw1NC42ODQ3NCAtMTUuNTQ4NDEsMi42ODc1OCAtMjkuNzY3NTIsOS4yNTQyOCAtNDEuNjY0ODcsMTguNjQzMiBoIDE1LjIzNCBjIDEyLjQ0NDU0LC03LjIyMjQ5IDI2Ljg5NTA4LC0xMS4zNzg2MSA0Mi4zNDIzMSwtMTEuMzc4NjEgMTUuNDU3NTUsMCAyOS45MjI1Miw0LjE0ODk3IDQyLjM3NTM0LDExLjM3ODYxIGggMTUuMTY3ODkgYyAtMi4wMzgyMywtMS42MDc3NCAtNC4xMTk2MSwtMy4xNjI0OSAtNi4yODk2NiwtNC41OTg4NiA2LjE0NjcyLC0xOC4wMTk3MyAyMy4xODc1OSwtMzAuOTM1OTIgNDMuMzIyNjQsLTMwLjkzNjA0IHYgLTguNTMxMjYgYyAtMjMuMTUzMjMsMS4yZS00IC00Mi45NjY3MywxNC41NzUzMyAtNTAuNzYzNCwzNS4wMjgyMSAtMy4wNTQyNywtMS42MzU3NCAtNi4yMDU4NywtMy4xMDc3MiAtOS40NTEwMSwtNC40MDA1IDkuNDMzMzIsLTI0LjAyMzg5IDMyLjgwODM0LC00MC45OTg1MSA2MC4yMTQ0MSwtNDAuOTk4NTEgdiAtOC41MzEyNiBjIC0zMS4wNDg0OCwwIC01Ny42Mjc0OCwxOS40MTg4NyAtNjguMjU1NDcsNDYuNzQ4NDEgLTMuMzMyMTEsLTAuOTgzMjEgLTYuNzQwOTQsLTEuNzgyMiAtMTAuMjE2NTksLTIuMzkwMzYgMTEuNzMyMjksLTMxLjk2MTU0IDQyLjQwOTMyLC01NC43MjMzMyA3OC40NzIwNiwtNTQuNzIzMzMgdiAtOC41MTQ2MyBjIC02LjY1NTQxLDAgLTEzLjE0NTgsMC43MTQ5IC0xOS40MDMyMiwyLjA1OTggLTEzLjk1MDAxLC0xOS4wNDg0NzQgLTM0LjkzODY0LC0zMi42MTgwMzYgLTU5LjEyMzkxLC0zNi44MzQ3MDQgMTEuNjkyOTksLTMyLjAxOTcwNCA0Mi40MDkxLC01NC45MDU5OSA3OC41MjcwOSwtNTQuOTEwNjAyIHYgLTguNTE0NzQ2IGMgLTEuODI5ODUsMi4yN2UtNCAtMy42NDg3MSwwLjA1NDA1IC01LjQ1MjQ2LDAuMTU5NDk2IC0xLjU2MjgzLC0xLjQ0ODI3NyAtMi41MzkwMSwtMy41MTk3MjMgLTIuNTM5MDEsLTUuODU0NTI2IDAsLTQuNDU5MDQ4NyAzLjUzMjQ2LC03Ljk5MTQ3MDggNy45OTE0NywtNy45OTE0NzA4IFYgMCBoIC0xMi45MjYyNSBjIC0yLjIzMjExLDIuODE2MjM5NCAtMy41Nzk5Niw2LjM2MDkwNzEgLTMuNTc5OTYsMTAuMjA1NTE4IDAsMi40NjA4ODggMC41NTEyOCw0LjgwNjU3NiAxLjUzMTA4LDYuOTEyIC0xLjUzMzA1LDAuMjUwOTYxIC0zLjA1MzQ4LDAuNTM4ODg1IC00LjU2MDIyLDAuODY0NjggQyAzNTYuMDc1LDE0LjkwNDg2OSAzNTMuNjI2MjQsMTEuOTcxMzg5IDM1MS4wMDkzNCw5LjE5NzcwNzEgMzUxLjEyOTE1LDUuOTQyODE1NyAzNTEuODQ1ODYsMi44NTM5NTkxIDM1My4wMTQxMSwwIGggLTguOTk5NCBjIC0wLjIwMjU4LDAuNjY4NTYwNjMgLTAuNDA5NywxLjMzNjkzMjMgLTAuNTcyNzgsMi4wMjEyNTM1IEMgMzQyLjYxODQxLDEuMzIyNzU5MSAzNDEuNzU2MjIsMC42Njk4NDU2NyAzNDAuOTA4NDMsMCBoIC0xNS4xNjc4NCBjIDkuMDk0MDMsNS4yOTM4MzMxIDE3LjEyMDMxLDEyLjIxOTA5OSAyMy42NDk1MiwyMC40MDAwMzggLTMuODQ5NjQsMS4yNTE5NjggLTcuNTkzLDIuNzQ1MzM1IC0xMS4yMDI0MSw0LjQ3MjIzOSBDIDMyNC45OTI1OCw5Ljg2MTgwNzkgMzA1Ljc2NjM5LDAuMjk2Mjc3MTcgMjg0LjMxODAyLDAgaCAtMi4wMzIyOSBDIDI2MC44NjU5OCwwLjI5NDQyNTIgMjQxLjY1OTA2LDkuODMxNzIyOCAyMjguNDYwMTIsMjQuODA2MTczIDIyNC44NDg5MywyMy4wODU2OTQgMjIxLjEwMjk3LDIxLjU5NTcyOSAyMTcuMjUyMiwyMC4zNTA0ODggMjIzLjc3NDQ5LDEyLjE4OTY1NyAyMzEuNzg2NjMsNS4yODM0MDE2IDI0MC44NjMyMSwwIGggLTE1LjE5NTQ0IGMgLTYuNTU2NjEsNS4xODUxMzM5IC0xMi40MjM0NiwxMS4yMDgwNzYgLTE3LjM4NzQyLDE3Ljk0MzcyMyAtNi4yMzg3OSwtMS4zMzY0NzkgLTEyLjcwODczLC0yLjA0MzMyNiAtMTkuMzQyNjcsLTIuMDQzMzI2IC02LjY2OTAyLDAgLTEzLjE3MjUzLDAuNzE1MDQ5IC0xOS40NDE3MSwyLjA2NTI4NSBDIDE2NC41Mjk4NiwxMS4yMjExOTEgMTU4LjY1OTg0LDUuMTkwODAzMSAxNTIuMDk3NTIsMCBoIC0xNS4xODk5NSBjIDkuMDg2NDMsNS4yODgzMTUgMTcuMTA2MjEsMTIuMjAxMjk4IDIzLjYzMzA4LDIwLjM3MjUyMyAtMy44NDk4MywxLjI0OTAyIC03LjU5MjU0LDIuNzQ4MTMyIC0xMS4yMDI0NSw0LjQ3MjEyNiBDIDEzNi4xNDM2MSw5Ljg0ODY5MjkgMTE2LjkyNzI0LDAuMjk1ODYxNDIgOTUuNDkwNTIsMCBIIDkzLjQ1Mjc1IEMgNzIuMDE5NjU0LDAuMjk1OTM3MDEgNTIuODAzNzgsOS44NDc0MDc5IDM5LjYwNTE0LDI0LjgzOTI0NCAzNS45OTQzNjksMjMuMTE1OTY5IDMyLjI1ODkxLDIxLjYyMDc4NyAyOC40MDgxMzksMjAuMzcyNTIzIDM0LjkzNDI0OSwxMi4yMDE2IDQyLjk1NTY1NCw1LjI4ODQyODMgNTIuMDQxMjIyLDAgSCAzNi44NjIzNzUgQyAzNi4wNDU0MywwLjY0NTk1OTA2IDM1LjIxMTU1MywxLjI3MjExMzQgMzQuNDE2OTA3LDEuOTQ0MTg5IDM0LjI1ODE2NywxLjI4NTAwMTYgMzQuMDU1NTg0LDAuNjQ0MzcxNjUgMzMuODYwNjc0LDAgaCAtOS4wMDQ4NzYgYyAxLjE1NTUxNSwyLjgyMjI4NjYgMS44NzM0MzcsNS44NzIxMzg2IDIuMDA0NzM3LDkuMDg3NDk2MSAtMi42NTE0NTIsMi44MDA0NDA5IC01LjEzMjQwOSw1Ljc2MTI4NDkgLTcuNDI0MjM5LDguODcyNzQyOSAtMS40NzU0OSwtMC4zMTc0OCAtMi45NjU5ODQsLTAuNTk3MDUyIC00LjQ2NjYwOCwtMC44NDI3MjEgMC45ODExNjYsLTIuMTA2NTk1IDEuNTM2NjQzLC00LjQ0OTI5OCAxLjUzNjY0MywtNi45MTIgQyAxNi41MDYzMzEsNi4zNjA5MDcxIDE1LjE1ODQzOCwyLjgxNjIzOTQgMTIuOTI2MzYyLDAgWiBtIDk0LjQ3MTYzNSw4LjU5MTgxMSBjIDE4LjYwOTQxNSwwIDM1LjM1MTU0NSw3Ljg1MzQ0MyA0Ny4xMTE4NDUsMjAuNDI3NjI4IC0zLjU0MTE5LDIuMTI5MzQ4IC02LjkyODE0LDQuNDg5ODE1IC0xMC4xMzk0NSw3LjA2MDY4NyAtOS40NzgyMiwtOS40NzE5MTIgLTIyLjU0OTQ5LC0xNS4zNDk2MDYgLTM2Ljk3MjM5NSwtMTUuMzQ5NjA2IC0xNC40MTU3OTksMCAtMjcuNDg1NTk0LDUuODY5NDkzIC0zNi45NjY5MTcsMTUuMzMzMDg5IEMgNTQuMjkyNzYyLDMzLjQ5MzE1MyA1MC45MDc0MDIsMzEuMTM2ODQ0IDQ3LjM2NTM0MiwyOS4wMDg0NzkgNTkuMTI5MzEsMTYuNDQwMTEzIDc1Ljg2NzI4OCw4LjU5MTgxMSA5NC40NzE2MzUsOC41OTE4MTEgWiBtIDE4OC44Mjc1MDUsMCBjIDE4LjYyNjE5LDAgMzUuMzgyOTUsNy44NjY1MiA0Ny4xNDQ4OCwyMC40NjA2NjEgLTMuNTQxMzEsMi4xMzIyMjEgLTYuOTI4NzEsNC40OTIwODIgLTEwLjEzOTQ1LDcuMDY2MjQzIC05LjQ4MTA2LC05LjQ5MjkyNiAtMjIuNTY0MzUsLTE1LjM4ODE5NSAtMzcuMDA1NDMsLTE1LjM4ODE5NSAtMTQuMzk3NjYsMCAtMjcuNDUwMDMsNS44NTczMjMgLTM2LjkyODQxLDE1LjMwMDA1NiAtMy4yMTUwMSwtMi41NjkzOTggLTYuNjA1NTEsLTQuOTI4MDUgLTEwLjE1MDQxLC03LjA1NTE2OCAxMS43NjI1LC0xMi41NDk5OTcgMjguNDg5OTMsLTIwLjM4MzU5NyA0Ny4wNzg4MiwtMjAuMzgzNTk3IHogbSAtOTQuMzYxNDYsMTUuODIzMzMyIGMgMzYuMTIxNDcsMCA2Ni44NDMwNiwyMi44MzM4NjUgNzguNTMyNjQsNTQuODc3NTY5IC0zLjQ3NzEyLDAuNTk3Njk0IC02Ljg5MzA2LDEuMzg0MTM4IC0xMC4yMjc1NSwyLjM1NzE3OCAtMTAuNTk2MjEsLTI3LjM5MzkwMyAtMzcuMjEwMiwtNDYuODY5NDY4IC02OC4zMDUwOSwtNDYuODY5NDY4IC0zMS4xMDUwNiwwIC01Ny43MjcwMywxOS40ODg2OCAtNjguMzE2MDIsNDYuODk2OTgzIC0zLjMzMzk2LC0wLjk3Njc4MSAtNi43NDUwMiwtMS43NjY3NCAtMTAuMjIyMTUsLTIuMzY4MTc3IDExLjY4NTE2LC0zMi4wNTIyMDggNDIuNDEwOCwtNTQuODk0MDg1IDc4LjUzODE3LC01NC44OTQwODUgeiBtIC05NC40NjYwNDUsNC45MjkyNiBjIDExLjg5MzYwNSwwIDIyLjY1NTAxNSw0LjcyNDgyNSAzMC41Mjg1NjUsMTIuMzk3NjA2IC0yLjY0NDEyLDIuNTUxMzcxIC01LjEzMjE0LDUuMjYzMjU3IC03LjQ1NzI3LDguMTEyNjQzIC01LjkzMTUyLC01Ljg5NTYwOSAtMTQuMDgxNjksLTkuNTYxMTQ2IC0yMy4wNzEyOTUsLTkuNTYxMTQ2IC04Ljk4MDQyMiwwIC0xNy4xMjYxNzQsMy42NTk1NjUgLTIzLjA2MDIyMSw5LjU0NDU5MiAtMi4zMjUxMjcsLTIuODUwMjkzIC00LjgxODMzMSwtNS41NTU0OSAtNy40NjI4MjgsLTguMTA3MTYzIDcuODc2MzQ2LC03LjY2NTk3OCAxOC42MzUxNDksLTEyLjM4NjUzMiAzMC41MjMwNDksLTEyLjM4NjUzMiB6IG0gMTg4LjgyNzUwNSwwIGMgMTEuOTE2MDUsMCAyMi42OTUxNSw0Ljc0MzE1NiAzMC41NzI1MiwxMi40NDE2MzggLTIuNjQyNzIsMi41NTM5MDIgLTUuMTM0MDcsNS4yNjAyNzEgLTcuNDU3Miw4LjExMjYwNSAtNS45MzUyMiwtNS45MjEyNzMgLTE0LjEwMzM0LC05LjYwNTE0IC0yMy4xMTUzMiwtOS42MDUxNCAtOC45NTcyOSwwIC0xNy4wODA1NiwzLjY0MjEwMyAtMjMuMDEwNzEsOS41MDA1OTggLTIuMzI3OTYsLTIuODQ4MjE0IC00LjgyMTMyLC01LjU1NzMwNCAtNy40NjgyNywtOC4xMDcyIDcuODcyNDksLTcuNjQxNTYyIDE4LjYxMjU4LC0xMi4zNDI1MDEgMzAuNDc4OTgsLTEyLjM0MjUwMSB6IG0gLTk0LjM2MTQ2LDEzLjk2NzE2OSBjIDI3LjQ0MzksMCA1MC44NDUxMSwxNy4wMjEyOTEgNjAuMjUyOTYsNDEuMDk3NjM3IC0zLjI0NzU2LDEuMjgyNjU5IC02LjQwNDMzLDIuNzQxODIxIC05LjQ2MjAxLDQuMzY3NDcxIC03Ljc3ODg3LC0yMC40ODc5NDkgLTI3LjYxMjMyLC0zNS4wOTQxOTggLTUwLjc5MDk1LC0zNS4wOTQyNzQgLTIzLjE5NDk3LDcuNmUtNSAtNDMuMDQwMjQsMTQuNjI3MjI1IC01MC44MDc0NCwzNS4xMzg0MTkgLTMuMDU2NzcsLTEuNjI5NTgxIC02LjIxNDksLTMuMDkyMDY5IC05LjQ2MjA4LC00LjM3ODU4MiA5LjM5OTA4LC0yNC4wOTQ1NjQgMzIuODEyODQsLTQxLjEzMDY3MSA2MC4yNjk1MiwtNDEuMTMwNjcxIHogbSAtOTQuNDY2MDQ1LDUuNjAxMjIyIGMgNy4xMjE2NTUsMCAxMy40ODcyNDUsMy4wNTQ3NjUgMTcuODk5Njk1LDcuOTE5OTYyIC0yLjE4MDQ5LDMuMjUyODUgLTQuMTYxNzYsNi42NTA2ODMgLTUuOTIwNzUsMTAuMTc4MDAzIC0yLjIzOTU2LC00LjM2NjUyNiAtNi43NzY0NjMsLTcuMzk2NjQ5IC0xMS45Nzg5NDUsLTcuMzk2NjQ5IC01LjE5NjMyMiwwIC05LjczNzIzNSwzLjAyMjAzNSAtMTEuOTg0NTA0LDcuMzgwMTMzIC0xLjc1NzQ4MSwtMy41Mjc4NDkgLTMuNzMwNDcsLTYuOTI1NTY5IC01LjkwOTY3LC0xMC4xNzgwNDEgNC40MTYyNjUsLTQuODU1Nzg2IDEwLjc4MDQ5OCwtNy45MDM0MDggMTcuODk0MTc0LC03LjkwMzQwOCB6IG0gMTg4LjgyNzUwNSwwIGMgNy4xNDk4NCwwIDEzLjUzNDk0LDMuMDgxNzEzIDE3Ljk0OTIsNy45ODA0NzIgLTIuMTgyOTQsMy4yNjQ2MDUgLTQuMTYyMjgsNi42NzU0NCAtNS45MjA2NywxMC4yMTY1OTIgLTIuMjIxMzgsLTQuNDE5NzA0IC02Ljc4NjgyLC03LjQ5NTc0OCAtMTIuMDI4NTMsLTcuNDk1NzQ4IC01LjE1NjIyLDAgLTkuNjY0NDQsMi45NzcxNzIgLTExLjkyOTQ0LDcuMjgwOTk2IC0xLjc1NzE4LC0zLjUxMTYzNSAtMy43MzQ0LC02Ljg5NTA2OCAtNS45MDk2MywtMTAuMTMzOTcyIDQuNDE0MTEsLTQuODIyMzc1IDEwLjc1MzQ0LC03Ljg0ODM0IDE3LjgzOTA3LC03Ljg0ODM0IHogbSAtOTQuMzYxNDYsMTMuMzAwODc1IGMgMjAuMTUxOTEsMS4xNGUtNCAzNy4yMDU0OCwxMi45MzY4NyA0My4zMzkyLDMwLjk4MDE0NSAtMy4wOTkyMiwyLjAzNzY1NyAtNi4wNzI2MSw0LjI1NDE5OSAtOC45MDAzLDYuNjM2NjI0IC0zLjcwMjA5LC0xNS41ODk2MDcgLTE3Ljc0MzUyLC0yNy4yNTE1MjggLTM0LjQzODksLTI3LjI1MTUyOCAtMTYuNzIyOTgsMCAtMzAuNzg3MjgsMTEuNjk5OTQ0IC0zNC40NjA5MSwyNy4zMjg1OTIgLTIuODI4MjUsLTIuMzg5NDkzIC01Ljc5OTM0LC00LjYxNDYxNCAtOC45MDAyOSwtNi42NTg1ODIgNi4xMTczOSwtMTguMDcyNTY3IDIzLjE4ODY5LC0zMS4wMzUxMzcgNDMuMzYxMiwtMzEuMDM1MjUxIHogbSAtOTQuNDY2MDQ1LDYuMDE5NzI5IGMgMi43MzA4NTksMCA0Ljg1MjE5NSwyLjEzNzg1MiA0Ljg1MjE5NSw0Ljg2ODcxMiAwLDIuNjA0NjYyIC0xLjkyOTcxMyw0LjY1MTg0MyAtNC40Nzc3Miw0LjgzNTY3OSAtMC4xMjUxMDIsLTUuMjllLTQgLTAuMjQ5MDcxLC0wLjAwMzggLTAuMzc0NTUxLC0wLjAwMzggLTAuMTI1MTAyLDAgLTAuMjQ5NDQ5LDAuMDAzOCAtMC4zNzQ1NTEsMC4wMDM4IC0yLjU0OTI1NCwtMC4xODQwNjMgLTQuNDk0MTk5LC0yLjIzMTAxNyAtNC40OTQxOTksLTQuODM1Njc5IDAsLTIuNzMwODYgMi4xMzc4NTIsLTQuODY4NzEyIDQuODY4NzEyLC00Ljg2ODcxMiB6IG0gMTg4LjgyNzUwNSwwIGMgMi43MzA4MiwwIDQuODUyMTUsMi4xMzc4NTIgNC44NTIxNSw0Ljg2ODcxMiAwLDIuNjA0NjYyIC0xLjkyOTc1LDQuNjUxODQzIC00LjQ3NzY0LDQuODM1Njc5IC0wLjEyNTEsLTUuMjllLTQgLTAuMjQ5MDcsLTAuMDAzOCAtMC4zNzQ1NSwtMC4wMDM4IC0wLjEyNTQ4LDAgLTAuMjQ5NDUsMC4wMDM4IC0wLjM3NDU1LDAuMDAzOCAtMi41NDczMywtMC4xODU1NzUgLTQuNDk0MiwtMi4yMzIzNzggLTQuNDk0MiwtNC44MzU2NzkgMCwtMi43MzA4NiAyLjEzNzg1LC00Ljg2ODcxMiA0Ljg2ODcxLC00Ljg2ODcxMiB6IG0gLTk0LjM2MTQ2LDEyLjg4MjIxOCBjIDE0LjU1MjYxLDAgMjYuMzMwMTEsMTEuNDM1OTgxIDI2Ljg2NjAxLDI1Ljg1MjY4NCAtMi43Mzk0LDIuODgzMDYgLTUuMjk4NzUsNS45Mzk4MyAtNy42NTU1NSw5LjE1MzYgLTEuNDY0NDEsLTAuMzExMDUgLTIuOTQ0NTUsLTAuNTg1NzUgLTQuNDMzNjEsLTAuODI2MiAxLjEwMTg5LC0yLjIwODQyIDEuNzI5NDQsLTQuNjg4OTIgMS43Mjk0NCwtNy4zMDg1MSAwLC05LjA2NjkgLTcuNDM5MzYsLTE2LjUwNjI5NiAtMTYuNTA2MjksLTE2LjUwNjI5NiAtOS4wNjY5NCwwIC0xNi41MDYyMiw3LjQzOTM5NiAtMTYuNTA2MjIsMTYuNTA2Mjk2IDAsMi42MTczNiAwLjYxODM3LDUuMTAxNiAxLjcxODM2LDcuMzA4NTEgLTEuNTIwMTMsMC4yNDU2NyAtMy4wMjczMywwLjUyOTA2IC00LjUyMTY4LDAuODQ4MjQgLTIuMzMwNjQsLTMuMTgwOTMgLTQuODU2MzUsLTYuMjA4MDcgLTcuNTYxOTIsLTkuMDY1NTQgMC40Nzk1OCwtMTQuNDY4NzUgMTIuMjgxNjUsLTI1Ljk2Mjc4NCAyNi44NzE0NiwtMjUuOTYyNzg0IHogbSAtOTQuNDY2MDQ1LDUuNDUyNTczIGMgMjYuODQ4NzQ1LDAgNTAuNzIzNTY1LDEyLjQ5NDc3OCA2Ni4yMDExODUsMzEuOTcxNTUxIC0zLjg1MzkxLDEuMjQyMjYgLTcuNTk5MjcsMi43MjY1OSAtMTEuMjEzNDgsNC40NDQ2MSAtMTMuNDA3MDgsLTE1LjMxNDExIC0zMy4wNzM2MywtMjUuMDA5OTYzIC01NC45ODc3NDMsLTI1LjAwOTk2MyAtMjEuOTE1ODU1LDAgLTQxLjU4NzA0OSw5LjY5ODk1MyAtNTQuOTk4NzQsMjUuMDE1NDEzIC0zLjYwOTkwMywtMS43MTc1IC03LjM0NzgxOCwtMy4yMDcyNyAtMTEuMTk2ODUxLC00LjQ1MDA2IEMgNDMuNzUyLDk5LjA2MzgzNiA2Ny42MjM1NzIsODYuNTY4MTg5IDk0LjQ3MTYzNSw4Ni41NjgxODkgWiBtIDE4OC44Mjc1MDUsMCBjIDI2Ljg1OTQ0LDAgNTAuNzQ2MDUsMTIuNTAzNTQ2IDY2LjIyMzE4LDMxLjk5MzQ3MSAtMy44NDk5LDEuMjQ2ODcgLTcuNTg2NzYsMi43NDQ5NiAtMTEuMTk2OTMsNC40NjY3MiAtMTMuNDA3OTEsLTE1LjMzODg3IC0zMy4wOTE3NiwtMjUuMDUzOTkzIC01NS4wMjYyNSwtMjUuMDUzOTkzIC0yMS44OTQ5NiwwIC00MS41NDk0NSw5LjY4MDM5MyAtNTQuOTYwMTksMjQuOTcxMzczIC0zLjYxNDc0LC0xLjcxMzg2IC03LjM1OTU0LC0zLjE5NTIxIC0xMS4yMTM0NCwtNC40MzM2NSAxNS40NzY2MywtMTkuNDU5OTE1IDM5LjMzODM0LC0zMS45NDM5MjEgNjYuMTczNjMsLTMxLjk0MzkyMSB6IG0gLTk0LjM2MTQ2LDEzLjQyNzQ1MiBjIDQuNDU5MDEsMCA3Ljk5MTU0LDMuNTMyNTM5IDcuOTkxNTQsNy45OTE1NDkgMCwyLjU2MDMyIC0xLjE3MTIsNC44MDUyMSAtMy4wMDcxLDYuMjU2NTkgLTEuNjUxNDMsLTAuMDg4NCAtMy4zMTEyOSwtMC4xMzIyOCAtNC45ODQ0NCwtMC4xMzIyOCAtMS42NzE2OSwwIC0zLjMzNDMsMC4wNDk1IC00Ljk4NDM3LDAuMTM3NTcgLTEuODM3MzgsLTEuNDUxMzggLTMuMDA3MTEsLTMuNzAwNjEgLTMuMDA3MTEsLTYuMjYyMTEgMCwtNC40NTkwMSAzLjUzMjQ2LC03Ljk5MTU0NiA3Ljk5MTQ4LC03Ljk5MTU0NiB6IG0gLTk0LjQ2NjA0NSw2LjU5ODE0OSBjIDE4LjY2OTgwNSwwIDM1LjQ2MTUzNSw3LjkwMjk5IDQ3LjIyNzU3NSwyMC41NDg3MiAtMy41NDQ3OCwyLjEyMjYyIC02LjkzNTI0LDQuNDc5MzkgLTEwLjE1MDUyLDcuMDQ0MjkgLTkuNDg2NSwtOS41MzM0NSAtMjIuNjAxMDEsLTE1LjQ1NDM4IC0zNy4wNzcwNTUsLTE1LjQ1NDM4IC0xNC40Nzg2NTIsMCAtMjcuNTk2OTM5LDUuOTIzMzkgLTM3LjA4ODA4OSwxNS40NTk4MiAtMy4yMTM5OTYsLTIuNTY1MDEgLTYuNjAxODUyLC00LjkyMDgzIC0xMC4xNDQ5MzIsLTcuMDQ0MjEgMTEuNzcwNDY5LC0xMi42NDgwNCAyOC41NjEzNjEsLTIwLjU1NDI0IDQ3LjIzMzAyMSwtMjAuNTU0MjQgeiBtIDE4OC44Mjc1MDUsMCBjIDE4LjY5MjIyLDAgMzUuNTAzNTIsNy45MjAzOCA0Ny4yNzE1MywyMC41OTI4MyAtMy41NDI5NywyLjEyNzMxIC02LjkzMTczLDQuNDg2MTEgLTEwLjE0NDkzLDcuMDU1MTcgLTkuNDkwNTgsLTkuNTY0MTMgLTIyLjYyNDA2LC0xNS41MDkzNyAtMzcuMTI2NiwtMTUuNTA5MzcgLTE0LjQ0Nzk3LDAgLTI3LjU0MTE5LDUuODk4MzcgLTM3LjAyNzU0LDE1LjM5OTI3IC0zLjIxODE2LC0yLjU2MjU2IC02LjYxMzk5LC00LjkxMzM1IC0xMC4xNjE0OSwtNy4wMzMyMSAxMS43NjgyNCwtMTIuNjE5OCAyOC41NDExNCwtMjAuNTA0NjkgNDcuMTg5MDMsLTIwLjUwNDY5IHogTSA5NC40NzE2MzUsMTI3LjM0NjI3IGMgMTEuOTQyODE1LDAgMjIuNzQ1NjE1LDQuNzYyMjQgMzAuNjI3NzA1LDEyLjQ5MTMgLTIuNjQ5NDksMi41NDgxOSAtNS4xNDM2LDUuMjU0MjIgLTcuNDczNzksOC4xMDE2NCAtNS45MzgzNiwtNS45NDM5NSAtMTQuMTIyMzksLTkuNjQzODQgLTIzLjE1MzkxNSwtOS42NDM4NCAtOS4wMzM1MjUsMCAtMTcuMjIyMjQ5LDMuNzAzMDcgLTIzLjE2NDkxNCw5LjY0OTI5IC0yLjMzMDY4MywtMi44NDc0NiAtNC44MjQwNzUsLTUuNTUyODUgLTcuNDczNzg5LC04LjEwMTY1IDcuODg2NDc2LC03LjczMTkzIDE4LjY5MzIwMywtMTIuNDk2NzQgMzAuNjM4NzAzLC0xMi40OTY3NCB6IG0gMTg4LjgyNzUwNSwwIGMgMTEuOTczNzMsMCAyMi44MDA5OCw0Ljc4NzMgMzAuNjg4MjUsMTIuNTUxODUgLTIuNjQ3NTIsMi41NTEwNiAtNS4xMzk5Myw1LjI1NzcgLTcuNDY4MzEsOC4xMDcyIC01Ljk0MzUzLC01Ljk4Mjc3IC0xNC4xNTQ4NiwtOS43MDk5NSAtMjMuMjE5OTQsLTkuNzA5OTUgLTguOTk3NzQsMCAtMTcuMTU2NDEsMy42NzI3OSAtMjMuMDkzMjksOS41Nzc2NiAtMi4zMzEyMiwtMi44NDI5NiAtNC44Mjk2NCwtNS41NDY1MyAtNy40NzkzMSwtOC4wOTA1NyA3Ljg4MDk2LC03LjY5NTk4IDE4LjY1ODQ3LC0xMi40MzYxOSAzMC41NzI2LC0xMi40MzYxOSB6IE0gOTQuNDcxNjM1LDE0Ni45MTQ4MSBjIDcuMTU5NTk1LDAgMTMuNTU2Mjk1LDMuMDg2NCAxNy45NzExOTUsNy45OTY5OSAtMi4xODk2MywzLjI1NzU0IC00LjE4MTc4LDYuNjU5NzUgLTUuOTQ4MSwxMC4xOTQ1MiAtMi4yMjI3LC00LjQxNjE5IC02Ljc4Mzk1MiwtNy40OTAzMSAtMTIuMDIzMDk1LC03LjQ5MDMxIC01LjIzOTkzNywwIC05LjgxMjQ0OCwzLjA3MzA2IC0xMi4wMzk2MSw3LjQ5MDMxIC0xLjc2Njg1MywtMy41MzE3OCAtMy43NTg0LC02LjkzMzI0IC01Ljk0ODE0NSwtMTAuMTg5IDQuNDE5NDc3LC00LjkxNDY0IDEwLjgyNDgzMiwtOC4wMDI1MSAxNy45ODc3NTUsLTguMDAyNTEgeiBtIDE4OC44Mjc1MDUsMCBjIDcuMTk5MDUsMCAxMy42MjU2NSwzLjEyMTc4IDE4LjA0Mjg2LDguMDc5NTcgLTIuMTk0NjIsMy4yNzAwNSAtNC4xOTA4Niw2LjY4NTQ2IC01Ljk1OTI2LDEwLjIzMzExIC0yLjE5OTQyLC00LjQ4MTM5IC02Ljc5NjU0LC03LjYxMTQ4IC0xMi4wODM2LC03LjYxMTQ4IC01LjE5MjAyLDAgLTkuNzI5NzksMy4wMTcwMSAtMTEuOTc5MDMsNy4zNjkxNCAtMS43NjQ4MSwtMy41MTgyNSAtMy43NDcwMiwtNi45MDc2NiAtNS45MzE2NiwtMTAuMTUwNTMgNC40MTY4MywtNC44NjU2MSAxMC43ODg1MSwtNy45MTk4MSAxNy45MTA2OSwtNy45MTk4MSB6IG0gLTE4OC44Mjc1MDUsMTkuMzIwNiBjIDIuNzMwODU5LDAgNC44NTIxOTUsMi4xMzc4NiA0Ljg1MjE5NSw0Ljg2ODcyIDAsMi42MDQ2NiAtMS45Mjk3MTMsNC42NTE2OSAtNC40Nzc3Miw0LjgzNTY0IC0wLjEyNTEwMiwtNS4zZS00IC0wLjI0OTA3MSwtMC4wMDQgLTAuMzc0NTUxLC0wLjAwNCAtMC4xMjUxMDIsMCAtMC4yNDk0NDksMC4wMDQgLTAuMzc0NTUxLDAuMDA0IC0yLjU0OTI1NCwtMC4xODQwNyAtNC40OTQxOTksLTIuMjMwOTggLTQuNDk0MTk5LC00LjgzNTY0IDAsLTIuNzMwODYgMi4xMzc4NTIsLTQuODY4NzIgNC44Njg3MTIsLTQuODY4NzIgeiBtIDE4OC44Mjc1MDUsMCBjIDIuNzMwODIsMCA0Ljg1MjE1LDIuMTM3ODYgNC44NTIxNSw0Ljg2ODcyIDAsMi42MDQ2NiAtMS45Mjk3NSw0LjY1MTY5IC00LjQ3NzY0LDQuODM1NjQgLTAuMTI1MSwtNS4zZS00IC0wLjI0OTA3LC0wLjAwNCAtMC4zNzQ1NSwtMC4wMDQgLTAuMTI1NDgsMCAtMC4yNDk0NSwwLjAwNCAtMC4zNzQ1NSwwLjAwNCAtMi41NDczMywtMC4xODU1OCAtNC40OTQyLC0yLjIzMjM4IC00LjQ5NDIsLTQuODM1NjQgMCwtMi43MzA4NiAyLjEzNzg1LC00Ljg2ODcyIDQuODY4NzEsLTQuODY4NzIgeiBNIDAsMTcwLjc3OTE2IHYgOC41MzY3MSBjIDExLjI1NzczOSwwIDIwLjgyODU5OCw2Ljg1OTU4IDI0Ljg0NDgsMTYuNjMyOSBoIDguOTk5MzU3IEMgMjkuNDMxMTgxLDE4MS40MTkyOSAxNS45Mzk3OCwxNzAuNzc5MTYgMCwxNzAuNzc5MTYgWiBtIDM3Ny44NzUzMSwwIGMgLTE1LjkzOTc4LDAgLTI5LjQzNjM5LDEwLjY0MDAxIC0zMy44NDk2MywyNS4xNjk2MSBoIDkuMDA0OTUgYyA0LjAxNTYzLC05Ljc3NDg0IDEzLjU4NTc3LC0xNi42MzI5IDI0Ljg0NDY4LC0xNi42MzI5IHogTSAwLDE4OS42ODExMSB2IDYuMjY3NjYgSCAxMi45MDQzMjggQyA5Ljg3MTc0OCwxOTIuMTQxMDUgNS4yMDgxODksMTg5LjY4MTExIDAsMTg5LjY4MTExIFogbSAzNzcuODc1MzEsMCBjIC01LjIwODExLDAgLTkuODcxNjMsMi40NTk5NCAtMTIuOTA0MjEsNi4yNjc2NiBoIDEyLjkwNDIxIHoiIC8+CiAgICA8L3BhdHRlcm4+CiAgICA8Y2xpcFBhdGgKICAgICAgIGNsaXBQYXRoVW5pdHM9InVzZXJTcGFjZU9uVXNlIgogICAgICAgaWQ9ImNsaXBQYXRoNTQiPgogICAgICA8cGF0aAogICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICBzdHlsZT0ic3Ryb2tlLXdpZHRoOjAuMDk5OTk5OTtzdHJva2UtbGluZWNhcDpzcXVhcmU7cGFpbnQtb3JkZXI6bWFya2VycyBmaWxsIHN0cm9rZTtzdG9wLWNvbG9yOiMwMDAwMDAiCiAgICAgICAgIGQ9Im0gMTY5MS4xOTk0LC03OTIuMzIwMDEgaCAxNjM0Ljg5MTkgdiA5Mi44MjgwNiBIIDE2OTEuMTk5NCBaIiAvPgogICAgPC9jbGlwUGF0aD4KICA8L2RlZnM+CiAgPGcKICAgICBpbmtzY2FwZTpsYWJlbD0iTGF5ZXIgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHBhdGgKICAgICAgIGQ9Ik0gMjUuMDAwMDI0LDQuOWUtNSBBIDI0Ljk5OTk1MSwyNC45OTk5NTEgMCAwIDEgNC45ZS01LDI1LjAwMDAyNCAyNC45OTk5NTEsMjQuOTk5OTUxIDAgMCAxIDI1LjAwMDAyNCw1MCAyNC45OTk5NTEsMjQuOTk5OTUxIDAgMCAxIDUwLDI1LjAwMDAyNCAyNC45OTk5NTEsMjQuOTk5OTUxIDAgMCAxIDI1LjAwMDAyNCw0LjllLTUgWiIKICAgICAgIHN0eWxlPSJmaWxsOnVybCgjcGF0dGVybjE4KTtzdHJva2Utd2lkdGg6NS4yOTE2NTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7cGFpbnQtb3JkZXI6c3Ryb2tlIGZpbGwgbWFya2VycztzdHJva2U6bm9uZTtmaWxsLW9wYWNpdHk6MSIKICAgICAgIGlkPSJwYXRoMyIgLz4KICA8L2c+Cjwvc3ZnPgo=
// ==/UserScript==
/* global require */
/* global $, jQuery */
/* global Node$1 */
/* 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', 'stop_at_row', 'delay_between_runs'
    ];
    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}}" }
            ]
        },
        "enable_uturn": {
            name: "Bật quay đầu",
            description: "Bật tính năng quay đầu cho đường đang chọn. Không cần tham số.",
            params: [
                { key: "empty", label: "không cần tham số", type: "{{value}}" }
            ]
        },
        "simplify_segment": {
            name: "Tối giản các segment",
            description: "Tối giản các segment đang chọn (giải quyết distortion)",
            params: [
                { key: "empty", label: "không cần tham số", type: "{{value}}" }
            ]
        },
        "cut_segment": {
            name: "Cắt các segment",
            description: "Cắt các segment đang chọn (giải quyết Loops)",
            params: [
                { key: "empty", label: "không cần tham số", type: "{{value}}" }
            ]
        },
        "venue_scale": {
            name: "Thay đổi kích thước của venue",
            description: "Thay đổi kích thước của venue đang chọn (giải quyết place too small). Dùng {{value}} để lấy từ ô nhập liệu.",
            params: [
                { key: "scale", label: "Kích thước", type: "number", placeholder: "550 m^2" }
            ]
        }
    };
    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_cut_segment": {
            name: "Cắt các segment",
            tasks: [
                {
                    taskId: "cut_segment",
                    enabled: true,
                    params: {
                        empty: "{{value}}"
                    }
                }
            ]
        },
        "wf_simplify_segment": {
            name: "Tối giản các segment (giải quyết distortion)",
            tasks: [
                {
                    taskId: "simplify_segment",
                    enabled: true,
                    params: {
                        empty: "{{value}}"
                    }
                }
            ]
        },
        "wf_scale_venue": {
            name: "Thay đổi kích thước place để hiện trên map (giải quyết too small)",
            tasks: [
                {
                    taskId: "venue_scale",
                    enabled: true,
                    params: {
                        scale: "550"
                    }
                },
                {
                    taskId: "update_lock_rank",
                    enabled: true,
                    params: {
                        rank: "3"
                    }
                }

            ]
        },
        "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_enable_uturn": {
            name: "Bật quay đầu ",
            tasks: [
                {
                    taskId: "enable_uturn",
                    enabled: true,
                    params: {
                        empty: "{{value}}"
                    }
                }
            ]
        },
        "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(); // REASON FOR BUG: This was removing sidebar button listeners after loading a file
        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) {
        if (!timeStr) return null;
        timeStr = timeStr.trim().toUpperCase();

        // Case 24h format: HH:MM
        if (/^\d{1,2}:\d{2}$/.test(timeStr)) {
            const [h, m] = timeStr.split(':').map(Number);
            if (h >= 0 && h <= 23 && m >= 0 && m <= 59) {
                return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
            }
            return null;
        }

        // Case Vietnamese 12h format: HH:MM SA|CH
        const parts = timeStr.split(/\s+/);
        if (parts.length !== 2) return null;

        let [hourMin, meridiem] = parts;
        let [hourStr, minuteStr] = hourMin.split(':');

        const hour = parseInt(hourStr, 10);
        const minute = parseInt(minuteStr, 10);
        if (isNaN(hour) || isNaN(minute)) return null;

        let hour24 = hour;
        if (meridiem === 'SA') {
            if (hour === 12) hour24 = 0;
        } else if (meridiem === 'CH') {
            if (hour !== 12) hour24 += 12;
        } else {
            return null;
        }

        return `${String(hour24).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
    }

    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) {
            log(`Lỗi chuyển đổi giờ từ "${openHoursString}" sang 24h.`, 'error');
            return null;
        }

        return [{
            days: [0, 1, 2, 3, 4, 5, 6],
            fromHour: from24h,
            toHour: to24h
        }];
    }

    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").replace(/đỗ xe/gi, "giữ xe");
            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);
        },
        cut_segment: async (parsedParams, selectedFeature) => {
            await delay(200)
            $("#WMETB_Split_Road").click()
        },
        simplify_segment: async (parsedParams, selectedFeature) => {
            await delay(500)
            $(".waze-btn.waze-btn-small.waze-btn-white.e85.e85-A").click();
            // simplify using turf
            const simplifiedGeometry = turf.simplify(selectedFeature.geometry, { tolerance: 0.00005 });
            wmeSDK.DataModel.Segments.updateSegment({
                segmentId: selectedFeature.id,
                geometry: simplifiedGeometry
            });
        },
        venue_scale: async (parsedParams, selectedFeature) => {
            const x = parseFloat(parsedParams.scale) || 550;
            try {
                const currentArea = turf.area(selectedFeature.geometry);
                let scale = Math.sqrt((x + 5) / currentArea);

                if (scale < 1 || currentArea > 600) return 0;

                let geometry = turf.transformScale(selectedFeature.geometry, scale);
                wmeSDK.DataModel.Venues.updateVenue({
                    venueId: selectedFeature.id,
                    geometry
                });

            } catch (e) {
                log('skipped', e);
            }
        },
        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;
        },
        enable_uturn: async (parsedParams, selectedFeature) => {
            let WazeActionSetTurn
            $(document).on('bootstrap.wme', () => {
                // Require Waze components with a check for .default (ES6 modules)
                const SetTurnModule = require('Waze/Model/Graph/Actions/SetTurn');
                WazeActionSetTurn = SetTurnModule.default || SetTurnModule;
            });
            const selection = wmeSDK.Editing.getSelection();
            function uturn(id) {
                function switchSegmentUturnHybrid(direction = 'A') {
                    // --- 1. Legacy W Model Method ---
                    if (typeof W !== 'undefined' && W.model && W.model.getTurnGraph && W.model.actionManager) {
                        try {
                            // Fix: Ensure constructor is loaded correctly
                            if (typeof WazeActionSetTurn !== 'function') {
                                const SetTurnModule = require('Waze/Model/Graph/Actions/SetTurn');
                                WazeActionSetTurn = SetTurnModule.default || SetTurnModule;
                            }

                            const seg = W.model.segments.getObjectById(id);
                            if (!seg || seg.isOneWay()) return log(`${GM_info.script.name}: skipped`, 'error');

                            const node = direction === 'A' ? seg.getFromNode() : seg.getToNode();

                            // Check current state
                            if (seg.isTurnAllowed(seg, node)) {
                                log(`${GM_info.script.name}: U-turn at ${direction} already allowed.`, 'info');
                                return log(`${GM_info.script.name}: already allowed`, 'info');
                            }

                            const turn = W.model.getTurnGraph().getTurnThroughNode(node, seg, seg);
                            if (!turn) return log(`${GM_info.script.name}: failed`, 'error');

                            W.model.actionManager.add(
                                new WazeActionSetTurn(
                                    W.model.getTurnGraph(),
                                    turn.withTurnData(turn.getTurnData().withState(1)) // 1 is ALLOW
                                )
                            );
                        } catch (e) {
                            log(`${GM_info.script.name}: Legacy U-turn error: ${e}`, 'error');
                        }
                    }

                    // --- 2. SDK Method Fallback ---
                    if (typeof wmeSDK !== 'undefined' && wmeSDK.DataModel && wmeSDK.DataModel.Turns) {
                        try {
                            const seg = wmeSDK.DataModel.Segments.getById({ segmentId: id });
                            if (!seg || !seg.isTwoWay) return log(`${GM_info.script.name}: skipped`, 'error');

                            const nodeId = direction === 'A' ? seg.fromNodeId : seg.toNodeId;
                            if (!wmeSDK.DataModel.Turns.canEditTurnsThroughNode({ nodeId })) return log(`${GM_info.script.name}: failed`, 'error');

                            if (wmeSDK.DataModel.Turns.isTurnAllowed({ fromSegmentId: seg.id, nodeId, toSegmentId: seg.id })) {
                                return log(`${GM_info.script.name}: already allowed`, 'info');
                            }

                            let turns = wmeSDK.DataModel.Turns.getTurnsThroughNode({ nodeId });
                            turns = turns.filter(turn => turn.isUTurn && turn.fromSegmentId === seg.id && turn.toSegmentId === seg.id);
                            if (turns.length === 0) return log(`${GM_info.script.name}: failed`, 'error');

                            for (let i = 0; i < turns.length; i++) {
                                wmeSDK.DataModel.Turns.updateTurn({ turnId: turns[i].id, isAllowed: true });
                            }
                        } catch (e) {
                            log(`${GM_info.script.name}: SDK U-turn error: ${e}`, 'error');
                        }
                    }
                    return log(`${GM_info.script.name}: failed`, 'error');
                }
                switchSegmentUturnHybrid('A');
                switchSegmentUturnHybrid('B');
            }
            selection.ids.forEach(id => {
                uturn(id)
            });
        },
        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 (new RegExp(parsedParams.provider, "i").test(name)) {
                name = name.replace(new RegExp(parsedParams.provider, "gi"), "").replace(/-/i, "").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(300);
        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);
                await delay(300);
            }
        }
        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];
        let selection = WazeWrap.getSelectedFeatures();

        // Retry tìm selection nếu đang chạy loop (vì WME có thể chậm)
        if (selection.length === 0 && isCalledByLoop) {
            for (let r = 0; r < 3; r++) {
                await delay(delay_between_runs);
                selection = WazeWrap.getSelectedFeatures();
                if (selection.length > 0) break;
            }
        }

        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 {
                const runDelay = parseInt(document.getElementById('delay_between_runs')?.value) || 1000;
                await processCurrentLink();
                if (!isLooping) { break; }
                await delay(runDelay); // Sử dụng giá trị từ cài đặt
                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');

                // Check stop at row condition
                const stopRow = parseInt(document.getElementById('stop_at_row')?.value) || 0;
                if (stopRow > 0 && (currentIndex + 1) === stopRow) {
                    log(`--- Đã đạt đến hàng dừng: ${stopRow} ---`, 'info');
                    isLooping = false;
                    break;
                }
            } catch (error) {
                if (isLooping) {
                    log(`Mục ${currentIndex + 1} thất bại: ${error.message}`, 'error');
                    updateStatus(`Lỗi: ${error.message}`);
                } 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(300);
            } 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 = /^https?:\/\//i.test(trimmedValue);
                        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. Vui lòng tải file trước.', 'warn');
            return;
        }
        try {
            log('Đang chuẩn bị dữ liệu Excel...', 'info');
            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;

            // Đảm bảo file lưu ra luôn có đuôi .xlsx để Excel không báo lỗi
            let fileName = currentFileName || 'workflow_results.xlsx';
            if (!fileName.toLowerCase().endsWith('.xlsx')) {
                fileName = fileName.replace(/\.[^/.]+$/, "") + ".xlsx";
            }

            a.download = fileName;
            document.body.appendChild(a); // Append to body for better Firefox compatibility
            a.click();

            setTimeout(() => {
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            }, 1000); // Delay revocation to ensure download starts

            hasUnsavedChanges = false;
            updateSaveButtonState();
            log(`✅ Đã lưu file: ${fileName}`, 'success');
        } catch (err) {
            log(`❌ Lỗi khi lưu file: ${err.message}`, 'error');
            console.error(err);
        }
    }
    /**
    * 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);
            p1.append(createElem("button", { id: "save_status_btn", class: "action-btn success", textContent: "💾 Lưu Status", style: "width:100%; margin-top:10px", disabled: true }));
            p1.append(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),
                createConfigRow("Delay giữa các lần run:", "number", "delay_between_runs", 1000),
                createConfigRow("Dừng tại hàng (row):", "number", "stop_at_row", 0),
                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; 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="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">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';
            isGasMode = !isLocal; // Cập nhật biến trạng thái quan trọng này
            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';
            log(`Chế độ: ${isLocal ? 'File Local' : 'Google Sheets'}`, 'info');
            updateUIState();
        };
        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;

        // Thay thế dragHandle.onmousedown bằng addEventListener
        dragHandle.addEventListener('mousedown', (e) => {
            e.preventDefault(); // Giờ đây hợp lệ vì passive: false
            isDragging = true;
            dragHandle.style.cursor = 'grabbing';

            const rect = elementToMove.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;

            const onMouseMove = (ev) => {
                if (!isDragging) return;
                elementToMove.style.left = (ev.clientX - offsetX) + 'px';
                elementToMove.style.top = (ev.clientY - offsetY) + 'px';
            };

            const onMouseUp = () => {
                isDragging = false;
                dragHandle.style.cursor = 'grab';
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            };

            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        }, { passive: false });
    }
    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() {
        const shortcuts = [
            { id: "wfe-next-link", keys: "Ctrl+39", desc: "WFE: Next Link", cb: () => navigate(1) },
            { id: "wfe-previous-link", keys: "Ctrl+37", desc: "WFE: Previous Link", cb: () => navigate(-1) },
            { id: "wfe-reselect-link", keys: "Ctrl+38", desc: "WFE: Reselect Current Link", cb: () => processCurrentLink() },
            { id: "wfe-run-workflow", keys: "Ctrl+40", desc: "WFE: Run Workflow", cb: () => runSelectedWorkflow(false) },
            { id: "wfe-mark-not-exist", keys: "AS+U", desc: "WFE: Mark as Not Exist", cb: () => { updateStatus('Không tồn tại'); WazeToastr.Alerts.info('WME WFE', 'Đã đánh dấu: Không tồn tại', false, false, 2000); } }
        ];

        shortcuts.forEach(({ id, keys, desc, cb }) => {
            try {
                if (wmeSDK.Shortcuts.isShortcutRegistered({ shortcutId: id })) return;
                wmeSDK.Shortcuts.createShortcut({
                    shortcutId: id,
                    shortcutKeys: keys,
                    description: desc,
                    callback: cb
                });
            } catch (e) {
                console.error(`WME Workflow Engine: Error registering shortcut ${id}`, e);
            }
        });
    }
    function navigate(direction, targetIndex = null) {
        placeholderCache.clear();
        if (isLooping) return;
        if (permalinks.length === 0) return;

        let newIndex = (targetIndex !== null) ? targetIndex : (currentIndex + direction);

        if (newIndex >= 0 && newIndex < permalinks.length) {

            const previousIndex = currentIndex;
            const navigationDirection = newIndex - currentIndex;

            if (previousIndex >= 0 && previousIndex !== newIndex) {
                const currentItemStatus = permalinks[previousIndex].status?.toLowerCase();
                const shouldSavePermalink = document.getElementById('save_permalink_after_create')?.checked;
                if (shouldSavePermalink && navigationDirection > 0) {
                    const newPermalink = wmeSDK.Map.getPermalink();
                    if (newPermalink) {
                        updatePermalinkInWorkbook(previousIndex, newPermalink);
                    } else {
                        log(`⚠️ Không lấy được Permalink cho mục ${previousIndex + 1}.`, 'warn');
                    }
                }
                if (currentItemStatus === 'đang tạo' || currentItemStatus === '') {
                    updateStatusByIndex(previousIndex, 'Đã tạo');
                }
            }

            currentIndex = newIndex;
            if (!permalinks[currentIndex].status || permalinks[currentIndex].status.toLowerCase() !== 'đang tạo') {
                updateStatus('Đang tạo');
                permalinks[currentIndex].status = 'Đang tạo';
            }

            updateUIState();
            processCurrentLink();
        }
    }
    /**
        * 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();
                wmeSDK.Events.on({
                    eventName: "wme-save-finished",
                    eventHandler: () => { 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);
        }
        await parseWazeUrlAndNavigate(item.url);
    }
    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() || 'New Permalink';
            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) {
        return new Promise(async (resolve, reject) => {
            try {
                const trimmedValue = value.trim();

                // 1. Kiểm tra tọa độ thuần (VD: "10.123, 106.456")
                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)) {
                        return reject(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 resolve();
                }

                // 2. Thử parse như WME URL (phải có param lat & lon)
                let isWmeLink = false;
                try {
                    const parsedUrl = new URL(trimmedValue);
                    const params = parsedUrl.searchParams;
                    if (params.get('lat') && params.get('lon')) {
                        isWmeLink = true;
                    }
                } catch (_) { }

                // 3. Nếu không phải WME link → follow redirect rồi gọi lại
                if (!isWmeLink && /^https?:\/\//i.test(trimmedValue)) {
                    log(`URL trung gian, đang follow redirect...`, 'info');
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: trimmedValue,
                        onload: async (res) => {
                            if (res.finalUrl && res.finalUrl !== trimmedValue) {
                                try {
                                    await parseWazeUrlAndNavigate(res.finalUrl);
                                    resolve();
                                } catch (e) { reject(e); }
                            } else {
                                log(`Không tìm thấy redirect. URL cuối: ${res.finalUrl || trimmedValue}`, 'warn');
                                resolve(); // Vẫn resolve để loop tiếp tục
                            }
                        },
                        onerror: (err) => {
                            log(`Lỗi khi follow redirect: ${trimmedValue}`, 'error');
                            reject(err);
                        }
                    });
                    return;
                } else if (!isWmeLink) {
                    return resolve(); // Không phải URL, có thể là rác
                }

                // 4. Xử lý WME link chuẩn
                const parsedUrl = new URL(trimmedValue);
                const params = parsedUrl.searchParams;
                const lon = parseFloat(params.get('lon'));
                const lat = parseFloat(params.get('lat'));
                const rawZoom = parseInt(params.get('zoomLevel') || params.get('zoom'), 10);
                const zoomInput = document.getElementById('coordinate_zoom');
                let zoom;
                if (rawZoom < 15) {
                    zoom = zoomInput ? parseInt(zoomInput.value, 10) : 20;
                } else {
                    zoom = rawZoom + 2;
                }
                W.map.setCenter(WazeWrap.Geometry.ConvertTo900913(lon, lat), zoom);
                const segmentIDs = (params.get('segments') || '').split(',').filter(id => id);
                const venueIDs = (params.get('venues') || '').split(',').filter(id => id);
                const totalIDs = segmentIDs.length + venueIDs.length;

                if (totalIDs === 0) return resolve();

                WazeWrap.Model.onModelReady(async () => {
                    const maxRetries = 15;
                    const retryDelay = 1000;

                    for (let attempt = 1; attempt <= maxRetries; attempt++) {
                        await delay(retryDelay);
                        let objectsToSelect = [];

                        if (segmentIDs.length > 0) {
                            const segments = segmentIDs.map(id => W.model.segments.getObjectById(id)).filter(Boolean);
                            objectsToSelect.push(...segments);
                        }
                        if (venueIDs.length > 0) {
                            const venues = venueIDs.map(id => W.model.venues.getObjectById(id)).filter(Boolean);
                            objectsToSelect.push(...venues);
                        }

                        if (objectsToSelect.length >= totalIDs) {
                            W.selectionManager.setSelectedModels(objectsToSelect);
                            await delay(300); // Đợi UI/Model cập nhật selection
                            return resolve();
                        }

                        if (attempt === maxRetries) {
                            if (objectsToSelect.length > 0) {
                                W.selectionManager.setSelectedModels(objectsToSelect);
                                await delay(300);
                                log(`Chỉ tìm thấy ${objectsToSelect.length}/${totalIDs} đối tượng sau ${maxRetries} lần thử.`, 'warn');
                                resolve();
                            } else {
                                reject(new Error('Không tìm thấy đối tượng'));
                            }
                        }
                    }
                }, true);
            } catch (error) {
                log(`Lỗi khi xử lý "${value}": ${error.message}`, 'error');
                reject(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': !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";
            nameInput.value = wf.name;
            idInput.value = workflowId;
            renderSdkTasksInEditor(wf.tasks || []);
        } else {
            title.textContent = "Tạo Workflow Mới";
            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;
                    isGasMode = (settings.data_source_mode === 'gas');
                    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();
})();