@necrolab/dashboard 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/.claude/settings.local.json +45 -0
  2. package/.eslintrc.js +24 -0
  3. package/.prettierignore +1 -0
  4. package/.prettierrc +10 -0
  5. package/.vscode/extensions.json +3 -0
  6. package/ICONS.md +21 -0
  7. package/README.md +65 -0
  8. package/backend/api.js +430 -0
  9. package/backend/auth.js +62 -0
  10. package/backend/batching.js +43 -0
  11. package/backend/endpoints.js +343 -0
  12. package/backend/index.js +23 -0
  13. package/backend/mock-data.js +66 -0
  14. package/backend/mock-src/classes/logger.js +112 -0
  15. package/backend/mock-src/classes/utils.js +42 -0
  16. package/backend/mock-src/ticketmaster.js +92 -0
  17. package/backend/validator.js +62 -0
  18. package/config/configs.json +20 -0
  19. package/config/filter.json +3 -0
  20. package/config/presale.csv +3 -0
  21. package/config/proxies.txt +6 -0
  22. package/config/used-codes.json +4 -0
  23. package/index.html +114 -0
  24. package/index.js +2 -0
  25. package/jsconfig.json +16 -0
  26. package/package.json +48 -0
  27. package/postcss.config.js +6 -0
  28. package/postinstall.js +9 -0
  29. package/public/android-chrome-192x192.png +0 -0
  30. package/public/android-chrome-512x512.png +0 -0
  31. package/public/apple-touch-icon.png +0 -0
  32. package/public/favicon-16x16.png +0 -0
  33. package/public/favicon-32x32.png +0 -0
  34. package/public/favicon.ico +0 -0
  35. package/public/flags/ae.svg +1 -0
  36. package/public/flags/at.svg +1 -0
  37. package/public/flags/au.svg +1 -0
  38. package/public/flags/be.svg +1 -0
  39. package/public/flags/ch.svg +1 -0
  40. package/public/flags/cz.svg +1 -0
  41. package/public/flags/de.svg +1 -0
  42. package/public/flags/dk.svg +1 -0
  43. package/public/flags/es.svg +1 -0
  44. package/public/flags/nl.svg +1 -0
  45. package/public/flags/no.svg +1 -0
  46. package/public/flags/nz.svg +1 -0
  47. package/public/flags/pl.svg +1 -0
  48. package/public/flags/se.svg +1 -0
  49. package/public/flags/uk.svg +1 -0
  50. package/public/flags/us.svg +1 -0
  51. package/public/img/award.svg +3 -0
  52. package/public/img/background.svg +14 -0
  53. package/public/img/bag_w.svg +12 -0
  54. package/public/img/banks/amex.svg +4 -0
  55. package/public/img/banks/mastercard.svg +4 -0
  56. package/public/img/banks/visa.svg +4 -0
  57. package/public/img/camera.svg +3 -0
  58. package/public/img/close.svg +3 -0
  59. package/public/img/controls/disable.svg +5 -0
  60. package/public/img/controls/enable.svg +5 -0
  61. package/public/img/groups.svg +3 -0
  62. package/public/img/hand.svg +3 -0
  63. package/public/img/key.svg +3 -0
  64. package/public/img/logo.png +0 -0
  65. package/public/img/logo_icon.png +0 -0
  66. package/public/img/logo_icon_2.png +0 -0
  67. package/public/img/logo_trans.png +0 -0
  68. package/public/img/loyalty.svg +3 -0
  69. package/public/img/mail.svg +3 -0
  70. package/public/img/pencil.svg +3 -0
  71. package/public/img/profile.svg +4 -0
  72. package/public/img/reload.svg +3 -0
  73. package/public/img/sandclock.svg +25 -0
  74. package/public/img/save.svg +5 -0
  75. package/public/img/savings.svg +3 -0
  76. package/public/img/scanner.svg +3 -0
  77. package/public/img/sell.svg +3 -0
  78. package/public/img/shield.svg +3 -0
  79. package/public/img/ski.svg +3 -0
  80. package/public/img/stadium.svg +8 -0
  81. package/public/img/stadium_w.svg +8 -0
  82. package/public/img/timer.svg +3 -0
  83. package/public/manifest.json +27 -0
  84. package/public/robots.txt +2 -0
  85. package/run +10 -0
  86. package/src/App.vue +307 -0
  87. package/src/assets/css/_input.scss +197 -0
  88. package/src/assets/css/main.scss +269 -0
  89. package/src/assets/css/tailwind.css +3 -0
  90. package/src/assets/img/award.svg +3 -0
  91. package/src/assets/img/background.svg +11 -0
  92. package/src/assets/img/camera.svg +3 -0
  93. package/src/assets/img/close.svg +3 -0
  94. package/src/assets/img/eyes/closed.svg +13 -0
  95. package/src/assets/img/eyes/open.svg +12 -0
  96. package/src/assets/img/groups.svg +3 -0
  97. package/src/assets/img/hand.svg +3 -0
  98. package/src/assets/img/key.svg +3 -0
  99. package/src/assets/img/logo.png +0 -0
  100. package/src/assets/img/logo_icon.png +0 -0
  101. package/src/assets/img/logo_icon_2.png +0 -0
  102. package/src/assets/img/logo_trans.png +0 -0
  103. package/src/assets/img/loyalty.svg +3 -0
  104. package/src/assets/img/mail.svg +3 -0
  105. package/src/assets/img/pencil.svg +3 -0
  106. package/src/assets/img/reload.svg +3 -0
  107. package/src/assets/img/savings.svg +3 -0
  108. package/src/assets/img/scanner.svg +3 -0
  109. package/src/assets/img/sell.svg +3 -0
  110. package/src/assets/img/shield.svg +3 -0
  111. package/src/assets/img/ski.svg +3 -0
  112. package/src/assets/img/square_check.svg +5 -0
  113. package/src/assets/img/square_uncheck.svg +5 -0
  114. package/src/assets/img/stadium.svg +8 -0
  115. package/src/assets/img/timer.svg +3 -0
  116. package/src/assets/img/wildcard.svg +7 -0
  117. package/src/components/Auth/LoginForm.vue +48 -0
  118. package/src/components/Editors/Account/Account.vue +119 -0
  119. package/src/components/Editors/Account/AccountCreator.vue +147 -0
  120. package/src/components/Editors/Account/AccountView.vue +87 -0
  121. package/src/components/Editors/Account/CreateAccount.vue +106 -0
  122. package/src/components/Editors/Profile/CreateProfile.vue +321 -0
  123. package/src/components/Editors/Profile/Profile.vue +142 -0
  124. package/src/components/Editors/Profile/ProfileCountryChooser.vue +75 -0
  125. package/src/components/Editors/Profile/ProfileView.vue +96 -0
  126. package/src/components/Editors/TagLabel.vue +16 -0
  127. package/src/components/Editors/TagToggle.vue +41 -0
  128. package/src/components/Filter/Filter.vue +409 -0
  129. package/src/components/Filter/FilterPreview.vue +236 -0
  130. package/src/components/Filter/PriceSortToggle.vue +105 -0
  131. package/src/components/Table/Header.vue +5 -0
  132. package/src/components/Table/Row.vue +5 -0
  133. package/src/components/Table/Table.vue +14 -0
  134. package/src/components/Table/index.js +4 -0
  135. package/src/components/Tasks/CheckStock.vue +62 -0
  136. package/src/components/Tasks/Controls/DesktopControls.vue +73 -0
  137. package/src/components/Tasks/Controls/MobileControls.vue +32 -0
  138. package/src/components/Tasks/Controls/index.js +3 -0
  139. package/src/components/Tasks/CreateTaskAXS.vue +339 -0
  140. package/src/components/Tasks/CreateTaskTM.vue +459 -0
  141. package/src/components/Tasks/MassEdit.vue +50 -0
  142. package/src/components/Tasks/QuickSettings.vue +167 -0
  143. package/src/components/Tasks/ScrapeVenue.vue +42 -0
  144. package/src/components/Tasks/Stats.vue +66 -0
  145. package/src/components/Tasks/Task.vue +296 -0
  146. package/src/components/Tasks/TaskLabel.vue +20 -0
  147. package/src/components/Tasks/TaskView.vue +126 -0
  148. package/src/components/Tasks/Utilities.vue +33 -0
  149. package/src/components/icons/Award.vue +8 -0
  150. package/src/components/icons/Bag.vue +8 -0
  151. package/src/components/icons/BagWhite.vue +8 -0
  152. package/src/components/icons/Box.vue +8 -0
  153. package/src/components/icons/Camera.vue +8 -0
  154. package/src/components/icons/Cart.vue +8 -0
  155. package/src/components/icons/Check.vue +5 -0
  156. package/src/components/icons/Checkmark.vue +11 -0
  157. package/src/components/icons/Click.vue +8 -0
  158. package/src/components/icons/Close.vue +21 -0
  159. package/src/components/icons/CloseX.vue +5 -0
  160. package/src/components/icons/Console.vue +13 -0
  161. package/src/components/icons/Down.vue +8 -0
  162. package/src/components/icons/Edit.vue +13 -0
  163. package/src/components/icons/Event.vue +8 -0
  164. package/src/components/icons/Expand.vue +8 -0
  165. package/src/components/icons/Filter.vue +13 -0
  166. package/src/components/icons/Gear.vue +8 -0
  167. package/src/components/icons/Group.vue +8 -0
  168. package/src/components/icons/Hand.vue +8 -0
  169. package/src/components/icons/Key.vue +21 -0
  170. package/src/components/icons/Logout.vue +13 -0
  171. package/src/components/icons/Loyalty.vue +8 -0
  172. package/src/components/icons/Mail.vue +8 -0
  173. package/src/components/icons/Menu.vue +8 -0
  174. package/src/components/icons/Pause.vue +5 -0
  175. package/src/components/icons/Pencil.vue +21 -0
  176. package/src/components/icons/Play.vue +8 -0
  177. package/src/components/icons/Plus.vue +8 -0
  178. package/src/components/icons/Profile.vue +18 -0
  179. package/src/components/icons/Reload.vue +7 -0
  180. package/src/components/icons/Sandclock.vue +33 -0
  181. package/src/components/icons/Savings.vue +8 -0
  182. package/src/components/icons/Scanner.vue +8 -0
  183. package/src/components/icons/Scrape.vue +8 -0
  184. package/src/components/icons/Sell.vue +21 -0
  185. package/src/components/icons/Shield.vue +8 -0
  186. package/src/components/icons/Shrink.vue +8 -0
  187. package/src/components/icons/Ski.vue +8 -0
  188. package/src/components/icons/Spinner.vue +42 -0
  189. package/src/components/icons/SquareCheck.vue +18 -0
  190. package/src/components/icons/SquareUncheck.vue +18 -0
  191. package/src/components/icons/Stadium.vue +13 -0
  192. package/src/components/icons/StadiumWhite.vue +13 -0
  193. package/src/components/icons/Status.vue +8 -0
  194. package/src/components/icons/Tag.vue +8 -0
  195. package/src/components/icons/Tasks.vue +13 -0
  196. package/src/components/icons/Ticket.vue +8 -0
  197. package/src/components/icons/Timer.vue +8 -0
  198. package/src/components/icons/Trash.vue +8 -0
  199. package/src/components/icons/Up.vue +10 -0
  200. package/src/components/icons/Wildcard.vue +18 -0
  201. package/src/components/icons/index.js +111 -0
  202. package/src/components/ui/Modal.vue +61 -0
  203. package/src/components/ui/Navbar.vue +207 -0
  204. package/src/components/ui/ReconnectIndicator.vue +90 -0
  205. package/src/components/ui/Splash.vue +24 -0
  206. package/src/components/ui/controls/CountryChooser.vue +87 -0
  207. package/src/components/ui/controls/EyeToggle.vue +11 -0
  208. package/src/components/ui/controls/atomic/Checkbox.vue +28 -0
  209. package/src/components/ui/controls/atomic/Dropdown.vue +138 -0
  210. package/src/components/ui/controls/atomic/LoadingButton.vue +45 -0
  211. package/src/components/ui/controls/atomic/MultiDropdown.vue +262 -0
  212. package/src/components/ui/controls/atomic/Switch.vue +84 -0
  213. package/src/libs/Filter.js +593 -0
  214. package/src/libs/ansii.js +565 -0
  215. package/src/libs/panzoom.js +1413 -0
  216. package/src/main.js +23 -0
  217. package/src/registerServiceWorker.js +32 -0
  218. package/src/router/index.js +65 -0
  219. package/src/stores/cities.json +1 -0
  220. package/src/stores/connection.js +399 -0
  221. package/src/stores/countries.js +88 -0
  222. package/src/stores/logger.js +103 -0
  223. package/src/stores/requests.js +88 -0
  224. package/src/stores/sampleData.js +1034 -0
  225. package/src/stores/ui.js +584 -0
  226. package/src/stores/utils.js +554 -0
  227. package/src/types/index.js +42 -0
  228. package/src/utils/debug.js +1 -0
  229. package/src/views/Accounts.vue +191 -0
  230. package/src/views/Console.vue +224 -0
  231. package/src/views/Editor.vue +785 -0
  232. package/src/views/FilterBuilder.vue +785 -0
  233. package/src/views/Login.vue +27 -0
  234. package/src/views/Profiles.vue +209 -0
  235. package/src/views/Tasks.vue +157 -0
  236. package/static/offline.html +184 -0
  237. package/tailwind.config.js +57 -0
  238. package/vite.config.js +63 -0
  239. package/vue.config.js +32 -0
  240. package/workbox-config.js +66 -0
@@ -0,0 +1,593 @@
1
+ const debug = false;
2
+ const log = (...args) => debug && console.log("[filter]", ...args);
3
+
4
+ const colors = {
5
+ HIGHLIGHT: "#d3f8e2",
6
+ SELECTED: "#a9def9",
7
+ SELECTED_EXPANDED: "#6bb6ff",
8
+ EXCLUDED: "#f694c1",
9
+ EXCLUDED_EXPANDED: "#f472b6",
10
+ UNSELECTABLE: "#311432"
11
+ };
12
+
13
+ const BASE_CSS = `.svg-wrapper path[generaladmission]:hover {fill: ${colors.HIGHLIGHT};pointer-events:all;cursor:pointer;}`;
14
+
15
+ const getAttributeValue = (element, attributeName) => {
16
+ try {
17
+ return element.attributes[attributeName]?.nodeValue?.trim() || "";
18
+ } catch {
19
+ return "";
20
+ }
21
+ };
22
+
23
+ const getTextContent = (element) => {
24
+ return element.innerHTML?.trim() || "";
25
+ };
26
+
27
+ const isWheelchair = (p) => {
28
+ if (p.row === "W" || p.sectionName === "W") return false;
29
+ return [("W", "ADA")].some((part) => p.row !== "W" && (p.row?.includes(part) || p.sectionName?.includes(part)));
30
+ };
31
+
32
+ const sortAlphaNum = (a, b) => {
33
+ var reA = /[^a-zA-Z]/g;
34
+ var reN = /[^0-9]/g;
35
+ var AInt = parseInt(a, 10);
36
+ var BInt = parseInt(b, 10);
37
+
38
+ if (isNaN(AInt) && isNaN(BInt)) {
39
+ var aA = a.replace(reA, "");
40
+ var bA = b.replace(reA, "");
41
+ if (aA === bA) {
42
+ var aN = parseInt(a.replace(reN, ""), 10);
43
+ var bN = parseInt(b.replace(reN, ""), 10);
44
+ return aN === bN ? 0 : aN > bN ? 1 : -1;
45
+ } else {
46
+ return aA > bA ? 1 : -1;
47
+ }
48
+ } else if (isNaN(AInt)) {
49
+ //A is not an Int
50
+ return 1; //to make alphanumeric sort first return -1 here
51
+ } else if (isNaN(BInt)) {
52
+ //B is not an Int
53
+ return -1; //to make alphanumeric sort first return 1 here
54
+ } else {
55
+ return AInt > BInt ? 1 : -1;
56
+ }
57
+ };
58
+
59
+ const uuidv4 = () => {
60
+ return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
61
+ (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
62
+ );
63
+ };
64
+
65
+ const cleanupFilter = (f) => {
66
+ if (f.priceSort === "null") f.priceSort = false;
67
+ const ret = {};
68
+ if (f.eventId) ret.eventId = f.eventId;
69
+ if (f.event) ret.eventId = f.event;
70
+ if (f.priceSort) ret.priceSort = f.priceSort;
71
+ if (typeof f.maxPrice === "number") ret.maxPrice = f.maxPrice;
72
+ if (typeof f.minPrice === "number") ret.minPrice = f.minPrice;
73
+ if (f.exclude) ret.exclude = true;
74
+
75
+ if (f.floor && !f.buyAny) {
76
+ ret.floor = true;
77
+ return ret;
78
+ }
79
+
80
+ // Catch-All
81
+ if (f.buyAny && !f.floor) {
82
+ ret.buyAny = true;
83
+ return ret;
84
+ }
85
+
86
+ // GA Catch-All
87
+ if (f.generalAdmission) {
88
+ ret.generalAdmission = true;
89
+ return ret;
90
+ }
91
+
92
+ if (f.section) ret.section = f.section;
93
+
94
+ // First Row
95
+ if (f.firstRow) {
96
+ ret.firstRow = true;
97
+ return ret;
98
+ }
99
+
100
+ if (f.rows && document.querySelectorAll(`path[section="${f.section}"]`)?.length === f.rows?.length) delete f.rows;
101
+ if (f.rows) ret.rows = f.rows.sort(sortAlphaNum);
102
+
103
+ return ret;
104
+ };
105
+
106
+ const getSectionNameMapping = () => {
107
+ const sectionRealName = {};
108
+
109
+ [...document.querySelectorAll("path")].forEach((t) => {
110
+ const sec = getAttributeValue(t, "section");
111
+ const secName = getAttributeValue(t, "sectionname");
112
+ if (sec && secName) {
113
+ sectionRealName[sec] = secName;
114
+ }
115
+ });
116
+
117
+ [...document.querySelectorAll("tspan")].forEach((t) => {
118
+ const sec = getAttributeValue(t, "section");
119
+ const content = getTextContent(t);
120
+
121
+ if (sec && content) {
122
+ if (!sectionRealName[sec]) {
123
+ sectionRealName[sec] = content;
124
+ } else if (!sectionRealName[sec].includes(content)) {
125
+ sectionRealName[sec] += " " + content;
126
+ }
127
+ }
128
+ });
129
+
130
+ log("getSectionNameMapping:", sectionRealName);
131
+ return sectionRealName;
132
+ };
133
+
134
+ const getReverseSectionNameMapping = () => {
135
+ const reverseSectionName = {};
136
+
137
+ [...document.querySelectorAll("path")].forEach((t) => {
138
+ const sec = getAttributeValue(t, "section");
139
+ const secName = getAttributeValue(t, "sectionname");
140
+
141
+ if (sec && secName) {
142
+ if (!reverseSectionName[secName]) {
143
+ reverseSectionName[secName] = [];
144
+ }
145
+ if (!reverseSectionName[secName].includes(sec)) {
146
+ reverseSectionName[secName].push(sec);
147
+ }
148
+ }
149
+ });
150
+
151
+ log("getReverseSectionNameMapping", reverseSectionName);
152
+ return reverseSectionName;
153
+ };
154
+
155
+ export default class FilterBuilder {
156
+ constructor() {
157
+ this.filters = [];
158
+ this.globalFilter = {
159
+ minPrice: 0,
160
+ maxPrice: 1000,
161
+ priceSort: "asc"
162
+ };
163
+ this.cssClasses = "";
164
+ this.temporaryCSS = "";
165
+
166
+ this.filterTypes = {
167
+ INVALID: -1,
168
+ CATCH_ALL: 0,
169
+ CATCH_ALL_GA: 1,
170
+ NORMAL: 2,
171
+ NORMAL_FIRSTROW: 3,
172
+ CATCH_ALL_FLOOR: 5
173
+ };
174
+
175
+ // Expanded filter - used for adding to a filter
176
+ this.expandedFilter = null;
177
+
178
+ // checked filter - used for max price bulk edit
179
+ this.selectedFilters = [];
180
+
181
+ this.reverseSectionNameMapping = {};
182
+ this.getSectionNameMapping = getSectionNameMapping;
183
+ this.updateHooks = [];
184
+ this.unselectable = [];
185
+ this.isDragging = false;
186
+ this.highlightedSeatColor = "#d3f8e2";
187
+ }
188
+
189
+ onUpdate(fn) {
190
+ this.updateHooks.push(fn);
191
+ }
192
+
193
+ setExpandedFilter(f) {
194
+ this.expandedFilter = f;
195
+ this.updateCss();
196
+ this.updateHooks.forEach((fn) => fn());
197
+ }
198
+
199
+ selectFilter(f) {
200
+ log("select filter", f);
201
+ if (!this.selectedFilters.includes(f)) this.selectedFilters.push(f);
202
+ }
203
+
204
+ unselectFilter(f) {
205
+ log("unselect filter", f);
206
+ this.selectedFilters = this.selectedFilters.filter((x) => x !== f);
207
+ }
208
+
209
+ getFilterType(filter) {
210
+ if (filter.buyAny) return this.filterTypes.CATCH_ALL;
211
+ else if (filter.floor) return this.filterTypes.CATCH_ALL_FLOOR;
212
+ else if (filter.generalAdmission) return this.filterTypes.CATCH_ALL_GA;
213
+ else if (filter.section) return this.filterTypes.NORMAL;
214
+ else return this.filterTypes.INVALID;
215
+ }
216
+
217
+ addRowHandlers() {
218
+ for (let i = 0; i < this.rows.length; i++) {
219
+ const row = this.rows[i];
220
+ row.onclick = () => {
221
+ const parsed = {
222
+ section: getAttributeValue(row, "section"),
223
+ row: getAttributeValue(row, "row"),
224
+ id: getAttributeValue(row, "id")
225
+ };
226
+
227
+ log(`Adding filter ${parsed.section}`);
228
+ const matchingFilter = this.filters.find(
229
+ (f) =>
230
+ this.isForCurrentEvent(f) &&
231
+ f.section === parsed.section &&
232
+ (f.rows?.includes(parsed.row) || !Array.isArray(f.rows))
233
+ );
234
+
235
+ if (matchingFilter) {
236
+ log(`Filter for ${parsed.section}/${parsed.row} already exists (${matchingFilter.id})`);
237
+ // Toggle expanded state
238
+ if (this.expandedFilter === matchingFilter.id) {
239
+ this.setExpandedFilter(null);
240
+ } else {
241
+ this.setExpandedFilter(matchingFilter.id);
242
+ }
243
+ return;
244
+ }
245
+
246
+ this.addFilter({
247
+ section: parsed.section,
248
+ event: this.currentEventId
249
+ });
250
+ };
251
+
252
+ row.onmouseover = () => {
253
+ const parsed = {
254
+ section: getAttributeValue(row, "section"),
255
+ row: getAttributeValue(row, "row"),
256
+ id: getAttributeValue(row, "id")
257
+ };
258
+
259
+ const matchingFilter = this.filters.find(
260
+ (f) =>
261
+ this.isForCurrentEvent(f) &&
262
+ f.section === parsed.section &&
263
+ (f.rows?.includes(parsed.row) || !Array.isArray(f.rows))
264
+ );
265
+
266
+ if (!matchingFilter) return;
267
+ if (Array.isArray(matchingFilter.rows))
268
+ matchingFilter.rows.forEach((row) => this.highlight({ section: parsed.section, row: row }));
269
+ else this.highlight({ section: matchingFilter.section });
270
+ };
271
+
272
+ row.onmouseout = () => {
273
+ if (!this.isDragging) this.clearHighlight();
274
+ };
275
+ }
276
+ }
277
+
278
+ reload(eventId) {
279
+ const paths = document.querySelectorAll("path");
280
+ this.rows = [...paths].filter((r) => r.id.startsWith("s_"));
281
+
282
+ this.addLabelHandlers();
283
+ this.addGAHandlers();
284
+ this.addRowHandlers();
285
+ this.currentEventId = eventId;
286
+
287
+ this.updateCss();
288
+ }
289
+
290
+ updateGlobalFilter(newValues) {
291
+ this.globalFilter = {
292
+ ...this.globalFilter,
293
+ ...newValues
294
+ };
295
+ }
296
+
297
+ addFilterToParent(filter, parent) {
298
+ filter.rows.forEach((r) => {
299
+ if (!parent.rows.includes(r)) parent.rows.push(r);
300
+ });
301
+ this.updateCss();
302
+ this.updateHooks.forEach((fn) => fn());
303
+ }
304
+
305
+ addFilter(filter) {
306
+ if (!this.filters) this.filters = [];
307
+
308
+ // Check if a filter with the same properties (excluding id) already exists
309
+ const existingFilter = this.filters.find((f) => {
310
+ const n = { ...f };
311
+ delete n.id;
312
+ return JSON.stringify(n) === JSON.stringify(filter);
313
+ });
314
+
315
+ if (existingFilter) {
316
+ console.log("Filter already exists:", existingFilter);
317
+ return; // Don't add the filter if it already exists
318
+ }
319
+
320
+ if (this.expandedFilter) {
321
+ const parent = this.filters.find((f) => f.event === filter.event && f.section === filter.section);
322
+ if (parent) return this.addFilterToParent(filter, parent);
323
+ }
324
+
325
+ this.filters.push({
326
+ id: uuidv4(),
327
+ ...filter
328
+ });
329
+
330
+ this.updateCss();
331
+ this.updateHooks.forEach((fn) => fn());
332
+ }
333
+
334
+ out() {
335
+ return {
336
+ globalFilter: {
337
+ priceSort: this.globalFilter.priceSort || null,
338
+ minPrice: Number(this.globalFilter.minPrice),
339
+ maxPrice: Number(this.globalFilter.maxPrice)
340
+ },
341
+ filters: this.filters.map(cleanupFilter)
342
+ };
343
+ }
344
+
345
+ log() {
346
+ const out = this.out();
347
+ out.filters.map((f) => (f._FILTER_TYPE = this.getFilterType(f)));
348
+ log(out);
349
+ }
350
+
351
+ swapFilterPos(a, b) {
352
+ this.filters.splice(a, 1, this.filters.splice(b, 1, this.filters[a])[0]);
353
+ }
354
+
355
+ reset(hard = false) {
356
+ this.filters = [];
357
+ this.rows = [];
358
+ if (hard) this.unselectable = [];
359
+ this.updateCss();
360
+ this.updateHooks.forEach((fn) => fn());
361
+ }
362
+
363
+ updateCss() {
364
+ this.cssClasses = BASE_CSS;
365
+ const gaSectionNameMapping = this.getSectionNameMapping();
366
+
367
+ this.filters.forEach((filter) => {
368
+ if (!this.isForCurrentEvent(filter)) return;
369
+
370
+ const tryFindRealName = Object.entries(gaSectionNameMapping).find(
371
+ ([, v]) => v === filter.section && v.includes(" ")
372
+ )?.[0];
373
+
374
+ if (tryFindRealName) {
375
+ log(`Real name for ${filter.section}: ${tryFindRealName}`);
376
+ } else {
377
+ log(filter.section, "is already real name", gaSectionNameMapping);
378
+ }
379
+
380
+ const type = this.getFilterType(filter);
381
+
382
+ let color;
383
+ if (filter.exclude) {
384
+ color = this.expandedFilter === filter.id ? colors.EXCLUDED_EXPANDED : colors.EXCLUDED;
385
+ } else {
386
+ color = this.expandedFilter === filter.id ? colors.SELECTED_EXPANDED : colors.SELECTED;
387
+ }
388
+
389
+ switch (type) {
390
+ case this.filterTypes.NORMAL:
391
+ // If it has no 'rows' property
392
+ if (!Array.isArray(filter.rows)) {
393
+ const isGA = document.querySelectorAll(`path[section='${filter.section}']`)?.length === 0;
394
+
395
+ if (isGA && tryFindRealName) {
396
+ this.cssClasses += `.svg-wrapper path[name="${tryFindRealName}"] {fill: ${color} !important;}\n`;
397
+ } else {
398
+ this.cssClasses += `.svg-wrapper path[section="${filter.section}"] {stroke: ${color} !important;}\n`;
399
+ }
400
+ } else {
401
+ this.cssClasses += filter.rows
402
+ .map(
403
+ (row) => `.svg-wrapper path[section="${filter.section}"][row="${row}"] {stroke: ${color} !important;}`
404
+ )
405
+ .join("\n") + "\n";
406
+ }
407
+ break;
408
+
409
+ case this.filterTypes.CATCH_ALL:
410
+ // Scope to SVG paths only to prevent global CSS leak
411
+ this.cssClasses += `.svg-wrapper path {stroke: ${color} !important;}\n`;
412
+ break;
413
+
414
+ case this.filterTypes.CATCH_ALL_GA:
415
+ this.cssClasses += `.svg-wrapper path[generaladmission] {fill: ${color} !important;}\n`;
416
+ break;
417
+
418
+ case this.filterTypes.CATCH_ALL_FLOOR:
419
+ // this.cssClasses += floors.map((f) => `path[name="${f}"] {fill: ${color} !important;}`).join("\n") + "\n";
420
+ break;
421
+
422
+ case this.filterTypes.INVALID:
423
+ default:
424
+ log("Can't add filter to CSS:", type, filter);
425
+ break;
426
+ }
427
+ });
428
+
429
+ this.unselectable.forEach((unselectable) => {
430
+ const [section, row] = unselectable.split("/");
431
+ log("UpdateCSS: adding unselectable", section, row);
432
+ const color = colors.UNSELECTABLE;
433
+ this.cssClasses += `.svg-wrapper path[section="${section}"][row="${row}"] {stroke: ${color} !important;}\n`;
434
+ });
435
+
436
+ log("Generated CSS:", this.cssClasses);
437
+ }
438
+
439
+ addLabelHandlers() {
440
+ const labels = document.querySelectorAll("tspan");
441
+ const gaSectionNameMapping = this.getSectionNameMapping();
442
+ log("addLabelHandlers: gaSectionNameMapping", gaSectionNameMapping);
443
+
444
+ for (let i = 0; i < labels.length; i++) {
445
+ const label = labels[i];
446
+ const section = getAttributeValue(label, "section");
447
+
448
+ label.onclick = () => {
449
+ // filter exists already for section - return
450
+ const matchingFilter = this.filters.find((f) => {
451
+ if (!this.isForCurrentEvent(f)) return false;
452
+ if (f.section !== section && f.section !== gaSectionNameMapping[section]) return false;
453
+ return true;
454
+ });
455
+
456
+ if (matchingFilter) {
457
+ log(`Filter for ${section} already exists (${matchingFilter.id})`);
458
+ // Toggle expanded state
459
+ if (this.expandedFilter === matchingFilter.id) {
460
+ this.setExpandedFilter(null);
461
+ } else {
462
+ this.setExpandedFilter(matchingFilter.id);
463
+ }
464
+ return;
465
+ }
466
+
467
+ if (isWheelchair({ section: gaSectionNameMapping[section] || section })) return;
468
+
469
+ const isGA = document.querySelectorAll(`path[sectionname="${section}"][row]`).length === 0;
470
+
471
+ try {
472
+ if (isGA) {
473
+ const realSectionName = gaSectionNameMapping[section];
474
+ if (realSectionName) {
475
+ this.addFilter({
476
+ section: realSectionName,
477
+ event: this.currentEventId
478
+ });
479
+ }
480
+ } else {
481
+ const reverseMapping = getReverseSectionNameMapping();
482
+ const sections = reverseMapping[section] || [section];
483
+ log("labelHandler: non-ga", sections, section);
484
+ sections.forEach((s) => {
485
+ this.addFilter({
486
+ section: s,
487
+ event: this.currentEventId
488
+ });
489
+ });
490
+ }
491
+ log("Added labelHandler filter", section);
492
+ } catch (e) {
493
+ log("Could not add labelHandler", section, e);
494
+ }
495
+ };
496
+ }
497
+ }
498
+
499
+ isForCurrentEvent(filter) {
500
+ return filter.event === this.currentEventId || filter.eventId === this.currentEventId;
501
+ }
502
+
503
+ addGAHandlers() {
504
+ const GASections = document.querySelectorAll("path[generaladmission=true]");
505
+ const gaSectionNameMapping = this.getSectionNameMapping();
506
+
507
+ for (let i = 0; i < GASections.length; i++) {
508
+ const path = GASections[i];
509
+ const section = getAttributeValue(path, "name");
510
+ const sectionName = gaSectionNameMapping[section];
511
+
512
+ path.onclick = () => {
513
+ // filter exists already for section
514
+ const matchingFilter = this.filters.find((f) => f.section === sectionName && this.isForCurrentEvent(f));
515
+ if (matchingFilter) {
516
+ log(`Filter for ${sectionName} (GA) already exists (${matchingFilter.id})`);
517
+ // Toggle expanded state
518
+ if (this.expandedFilter === matchingFilter.id) {
519
+ this.setExpandedFilter(null);
520
+ } else {
521
+ this.setExpandedFilter(matchingFilter.id);
522
+ }
523
+ return;
524
+ }
525
+ log(`GAHandler: adding filter for ${section} (realname: ${sectionName})`, section, sectionName);
526
+ this.addFilter({
527
+ section: sectionName,
528
+ event: this.currentEventId
529
+ });
530
+ };
531
+ }
532
+ }
533
+
534
+ deleteFilterById(id) {
535
+ this.filters = this.filters.filter((f) => f.id !== id);
536
+ // this.updateHooks = this.updateHooks.filter((i) => id !== i);
537
+ if (this.expandedFilter === id) this.expandedFilter = "";
538
+ this.updateCss();
539
+ this.updateHooks.forEach((fn) => fn());
540
+ }
541
+
542
+ replaceById(id, filter) {
543
+ if (this.selectedFilters.length !== 0 && typeof filter.maxPrice === "number") {
544
+ this.selectedFilters.forEach((id) => {
545
+ let filterIndex = this.filters.findIndex((f) => f.id === id);
546
+ this.filters[filterIndex] = { ...this.filters[filterIndex], ...filter };
547
+ });
548
+ }
549
+ const index = this.filters.findIndex((f) => f.id === id);
550
+ if (index === -1) return false;
551
+ this.filters[index] = {
552
+ ...this.filters[index],
553
+ ...filter,
554
+ id: this.filters[index].id
555
+ };
556
+ this.updateCss();
557
+ this.updateHooks.forEach((fn) => fn());
558
+ return this.filters[index];
559
+ }
560
+
561
+ updateByIndex(i, filter) {
562
+ this.filters[i] = {
563
+ ...this.filters[i],
564
+ ...filter
565
+ };
566
+ this.updateCss();
567
+ this.updateHooks.forEach((fn) => fn());
568
+ }
569
+
570
+ highlight(filter, isGA = false) {
571
+ if (filter.row && this.isUnselectable(filter.sec, filter.row)) return;
572
+
573
+ let newCSS = "";
574
+
575
+ if (isGA) {
576
+ if (this.filters.find((f) => f.section === filter.name)) return;
577
+ newCSS += `.svg-wrapper path[name="${filter.section || filter.name}"] {fill: ${colors.HIGHLIGHT} !important}`;
578
+ } else if (filter.row) {
579
+ newCSS += `.svg-wrapper path[section="${filter.section}"][row="${filter.row}"] {stroke: ${colors.HIGHLIGHT} !important}`;
580
+ } else {
581
+ newCSS += `.svg-wrapper path[section="${filter.section}"] {stroke: ${colors.HIGHLIGHT} !important}`;
582
+ }
583
+ this.temporaryCSS += newCSS;
584
+ }
585
+
586
+ clearHighlight() {
587
+ this.temporaryCSS = "";
588
+ }
589
+
590
+ isUnselectable(section, row) {
591
+ return this.unselectable.includes(`${section}/${row}`);
592
+ }
593
+ }