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.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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