@necrolab/dashboard 0.5.33 → 0.5.35
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.
- package/package.json +1 -1
- package/src/assets/css/components/headers.scss +10 -4
- package/src/assets/css/components/toasts.scss +17 -1
- package/src/assets/css/main.scss +1 -0
- package/src/components/Tasks/Stats.vue +352 -25
- package/src/components/Tasks/Task.vue +10 -4
- package/src/components/Tasks/TaskView.vue +52 -38
- package/src/libs/Filter.js +0 -4
- package/src/main.js +5 -0
- package/src/stores/connection.js +4 -2
- package/src/stores/ui.js +110 -5
- package/src/views/Tasks.vue +28 -26
package/package.json
CHANGED
|
@@ -7,19 +7,25 @@
|
|
|
7
7
|
@apply flex items-center justify-between;
|
|
8
8
|
padding-top: 0.75rem;
|
|
9
9
|
padding-bottom: 0.5rem;
|
|
10
|
-
flex-wrap: nowrap;
|
|
10
|
+
flex-wrap: nowrap !important;
|
|
11
11
|
gap: 0.5rem;
|
|
12
|
+
min-height: 0;
|
|
12
13
|
|
|
13
14
|
.page-header-card {
|
|
14
15
|
@apply flex items-center gap-2.5 rounded-lg;
|
|
15
16
|
padding: 0.375rem 0.75rem;
|
|
16
17
|
background: linear-gradient(135deg, var(--color-gradient-start) 0%, var(--color-gradient-end) 100%);
|
|
17
18
|
border: 1px solid var(--color-border-light);
|
|
18
|
-
flex: 0
|
|
19
|
+
flex: 0 0 auto;
|
|
19
20
|
min-width: 0;
|
|
20
|
-
max-width: 60%;
|
|
21
21
|
white-space: nowrap;
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
/* Mobile - allow shrinking if needed */
|
|
24
|
+
@media (max-width: 767px) {
|
|
25
|
+
flex: 0 1 auto;
|
|
26
|
+
max-width: calc(100% - 180px);
|
|
27
|
+
gap: 0.5rem;
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
svg, img {
|
|
25
31
|
width: 17px;
|
|
@@ -38,17 +38,33 @@
|
|
|
38
38
|
.Toastify__toast-container {
|
|
39
39
|
pointer-events: none;
|
|
40
40
|
--toastify-toast-bd-radius: 6px;
|
|
41
|
+
|
|
42
|
+
/* Mobile responsive - prevent overflow and edge-to-edge toasts */
|
|
43
|
+
@media (max-width: 767px) {
|
|
44
|
+
width: calc(100vw - 2rem) !important;
|
|
45
|
+
max-width: 360px;
|
|
46
|
+
right: 1rem !important;
|
|
47
|
+
left: auto !important;
|
|
48
|
+
padding: 0 !important;
|
|
49
|
+
top: calc(env(safe-area-inset-top) + 1rem) !important;
|
|
50
|
+
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
.Toastify__toast {
|
|
44
54
|
min-height: 50px !important;
|
|
45
|
-
height:
|
|
55
|
+
height: auto !important;
|
|
46
56
|
pointer-events: auto !important;
|
|
47
57
|
margin-bottom: 6px !important;
|
|
48
58
|
|
|
49
59
|
transition: transform 0.18s cubic-bezier(0.25, 0.1, 0.25, 1), opacity 0.12s cubic-bezier(0.25, 0.1, 0.25, 1) !important;
|
|
50
60
|
transform: translate3d(0, 0, 0);
|
|
51
61
|
|
|
62
|
+
/* Mobile responsive - ensure proper sizing */
|
|
63
|
+
@media (max-width: 767px) {
|
|
64
|
+
margin-bottom: 8px !important;
|
|
65
|
+
border-radius: 6px !important;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
&.Toastify__slide-enter-active {
|
|
53
69
|
animation: slideInRight 0.22s cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
54
70
|
}
|
package/src/assets/css/main.scss
CHANGED
|
@@ -114,6 +114,7 @@ svg:not([stroke]):not([fill="none"]) polygon:not([fill]) {
|
|
|
114
114
|
|
|
115
115
|
.mobile-icons {
|
|
116
116
|
@apply ml-auto flex items-center gap-x-2 lg:hidden;
|
|
117
|
+
flex-shrink: 0;
|
|
117
118
|
|
|
118
119
|
button {
|
|
119
120
|
@apply flex h-8 w-8 items-center justify-center rounded transition-all duration-150 bg-dark-400;
|
|
@@ -1,73 +1,400 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="queue-stats"
|
|
4
|
+
:class="[{ 'queue-stats--full': visibleCards === 4 }, $attrs.class]"
|
|
5
|
+
:style="statsGridStyle"
|
|
6
|
+
v-if="ui.queueStats.show"
|
|
7
|
+
:key="key">
|
|
3
8
|
<div
|
|
4
9
|
v-if="ui.queueStats.total"
|
|
5
|
-
class="stat-badge">
|
|
6
|
-
<h2 class="
|
|
10
|
+
class="stat-badge queue-stats__card queue-stats__card--metric">
|
|
11
|
+
<h2 class="queue-stats__header">
|
|
7
12
|
<img width="14px" src="@/assets/img/wildcard.svg" />
|
|
8
|
-
<span class="hidden md:inline">Total</span>
|
|
9
13
|
</h2>
|
|
10
|
-
<span class="
|
|
14
|
+
<span class="queue-stats__value">
|
|
11
15
|
{{ ui.queueStats.total }}
|
|
12
16
|
</span>
|
|
13
17
|
</div>
|
|
14
18
|
<div
|
|
15
19
|
v-if="ui.queueStats.queued"
|
|
16
|
-
class="stat-badge">
|
|
17
|
-
<h2 class="
|
|
20
|
+
class="stat-badge queue-stats__card queue-stats__card--metric">
|
|
21
|
+
<h2 class="queue-stats__header">
|
|
18
22
|
<SkiIcon />
|
|
19
|
-
<span class="hidden md:inline">Queued</span>
|
|
20
23
|
</h2>
|
|
21
|
-
<span class="
|
|
24
|
+
<span class="queue-stats__value">
|
|
22
25
|
{{ ui.queueStats.queued }}
|
|
23
26
|
</span>
|
|
24
27
|
</div>
|
|
25
28
|
<div
|
|
26
29
|
v-if="ui.queueStats.sleeping"
|
|
27
|
-
class="stat-badge">
|
|
28
|
-
<h2 class="
|
|
30
|
+
class="stat-badge queue-stats__card queue-stats__card--metric">
|
|
31
|
+
<h2 class="queue-stats__header">
|
|
29
32
|
<TimerIcon />
|
|
30
|
-
<span class="hidden md:inline">Sleeping</span>
|
|
31
33
|
</h2>
|
|
32
|
-
<span class="
|
|
34
|
+
<span class="queue-stats__value">
|
|
33
35
|
{{ ui.queueStats.sleeping }}
|
|
34
36
|
</span>
|
|
35
37
|
</div>
|
|
36
38
|
<div
|
|
37
39
|
v-if="ui.queueStats.nextQueuePasses.length > 0"
|
|
38
|
-
class="
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
<span class="block sm:hidden">Pass</span>
|
|
40
|
+
class="stat-badge queue-stats__card queue-stats__card--passes"
|
|
41
|
+
:title="ui.queueStats.nextQueuePasses.join(', ')">
|
|
42
|
+
<h2 class="queue-stats__header">
|
|
43
|
+
<SandclockIcon />
|
|
43
44
|
</h2>
|
|
44
|
-
<span class="
|
|
45
|
-
{{
|
|
45
|
+
<span class="queue-stats__value queue-stats__value--passes">
|
|
46
|
+
<span class="queue-stats__pass-text">{{ queuePassPreviewText }}</span>
|
|
47
|
+
<button
|
|
48
|
+
v-if="hiddenPassCount > 0"
|
|
49
|
+
type="button"
|
|
50
|
+
class="queue-stats__more-btn"
|
|
51
|
+
@click.stop="openPassesOverlay">
|
|
52
|
+
+{{ hiddenPassCount }}
|
|
53
|
+
</button>
|
|
46
54
|
</span>
|
|
47
55
|
</div>
|
|
48
56
|
</div>
|
|
57
|
+
|
|
58
|
+
<Teleport to="body">
|
|
59
|
+
<div
|
|
60
|
+
v-if="showPassesOverlay"
|
|
61
|
+
class="queue-pass-overlay"
|
|
62
|
+
@click.self="closePassesOverlay">
|
|
63
|
+
<div class="queue-pass-modal">
|
|
64
|
+
<div class="queue-pass-modal__header">
|
|
65
|
+
<h3>Next Queue Passes</h3>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
class="queue-pass-modal__close"
|
|
69
|
+
@click="closePassesOverlay">
|
|
70
|
+
Close
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="queue-pass-modal__body">
|
|
74
|
+
<ul class="queue-pass-modal__list">
|
|
75
|
+
<li
|
|
76
|
+
v-for="(pass, index) in ui.queueStats.nextQueuePasses"
|
|
77
|
+
:key="`${pass}-${index}`"
|
|
78
|
+
class="queue-pass-modal__item">
|
|
79
|
+
<span>#{{ index + 1 }}</span>
|
|
80
|
+
<span>{{ pass }}</span>
|
|
81
|
+
</li>
|
|
82
|
+
</ul>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</Teleport>
|
|
49
87
|
</template>
|
|
50
88
|
|
|
51
89
|
<script setup>
|
|
52
|
-
import { SkiIcon, TimerIcon,
|
|
90
|
+
import { SkiIcon, TimerIcon, SandclockIcon } from "@/components/icons";
|
|
53
91
|
import { useUIStore } from "@/stores/ui";
|
|
54
|
-
import { ref, onUnmounted } from "vue";
|
|
92
|
+
import { ref, onUnmounted, computed, onMounted } from "vue";
|
|
93
|
+
|
|
94
|
+
defineOptions({
|
|
95
|
+
inheritAttrs: false
|
|
96
|
+
});
|
|
55
97
|
|
|
56
98
|
const ui = useUIStore();
|
|
57
99
|
|
|
58
100
|
const getQueuePassAmount = (width) => {
|
|
59
|
-
if (width
|
|
60
|
-
if (width
|
|
101
|
+
if (width >= 1536) return 10;
|
|
102
|
+
if (width >= 1280) return 8;
|
|
103
|
+
if (width >= 1024) return 6;
|
|
104
|
+
if (width >= 768) return 4;
|
|
105
|
+
if (width >= 640) return 2;
|
|
61
106
|
return 1;
|
|
62
107
|
};
|
|
108
|
+
const getQueuePassCharBudget = (width) => {
|
|
109
|
+
if (width >= 1536) return 56;
|
|
110
|
+
if (width >= 1280) return 42;
|
|
111
|
+
if (width >= 1024) return 32;
|
|
112
|
+
if (width >= 768) return 24;
|
|
113
|
+
if (width >= 640) return 16;
|
|
114
|
+
return 10;
|
|
115
|
+
};
|
|
116
|
+
const buildQueuePassPreview = (passes, maxItems, maxChars) => {
|
|
117
|
+
const values = passes.slice(0, maxItems).map((value) => String(value));
|
|
118
|
+
if (!values.length) return "";
|
|
119
|
+
|
|
120
|
+
let currentLength = 0;
|
|
121
|
+
const output = [];
|
|
122
|
+
|
|
123
|
+
for (const value of values) {
|
|
124
|
+
const separator = output.length ? 2 : 0;
|
|
125
|
+
const nextLength = currentLength + separator + value.length;
|
|
126
|
+
if (nextLength > maxChars) break;
|
|
127
|
+
output.push(value);
|
|
128
|
+
currentLength = nextLength;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (output.length === 0) output.push(values[0]);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
visible: output,
|
|
135
|
+
hiddenCount: Math.max(passes.length - output.length, 0)
|
|
136
|
+
};
|
|
137
|
+
};
|
|
63
138
|
|
|
64
139
|
let key = ref(0);
|
|
140
|
+
const width = ref(window.innerWidth);
|
|
65
141
|
let queuePassAmount = ref(getQueuePassAmount(window.innerWidth));
|
|
142
|
+
const queuePassCharBudget = ref(getQueuePassCharBudget(window.innerWidth));
|
|
143
|
+
const showPassesOverlay = ref(false);
|
|
144
|
+
const visibleCards = computed(
|
|
145
|
+
() =>
|
|
146
|
+
Number(Boolean(ui.queueStats.total)) +
|
|
147
|
+
Number(Boolean(ui.queueStats.queued)) +
|
|
148
|
+
Number(Boolean(ui.queueStats.sleeping)) +
|
|
149
|
+
Number(Boolean(ui.queueStats.nextQueuePasses.length))
|
|
150
|
+
);
|
|
151
|
+
const statsGridStyle = computed(() => ({
|
|
152
|
+
"--stats-card-count": `${Math.max(visibleCards.value, 1)}`
|
|
153
|
+
}));
|
|
154
|
+
const queuePassPreviewData = computed(() =>
|
|
155
|
+
buildQueuePassPreview(ui.queueStats.nextQueuePasses, queuePassAmount.value, queuePassCharBudget.value)
|
|
156
|
+
);
|
|
157
|
+
const queuePassPreviewText = computed(() => queuePassPreviewData.value.visible.join(", "));
|
|
158
|
+
const hiddenPassCount = computed(() => queuePassPreviewData.value.hiddenCount);
|
|
159
|
+
|
|
160
|
+
const openPassesOverlay = () => {
|
|
161
|
+
if (!hiddenPassCount.value) return;
|
|
162
|
+
showPassesOverlay.value = true;
|
|
163
|
+
};
|
|
66
164
|
|
|
67
|
-
const
|
|
165
|
+
const closePassesOverlay = () => {
|
|
166
|
+
showPassesOverlay.value = false;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleResize = () => {
|
|
170
|
+
width.value = window.innerWidth;
|
|
171
|
+
queuePassAmount.value = getQueuePassAmount(width.value);
|
|
172
|
+
queuePassCharBudget.value = getQueuePassCharBudget(width.value);
|
|
173
|
+
};
|
|
68
174
|
window.addEventListener("resize", handleResize);
|
|
69
175
|
|
|
176
|
+
const handleKeyDown = (event) => {
|
|
177
|
+
if (event.key === "Escape" && showPassesOverlay.value) {
|
|
178
|
+
closePassesOverlay();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
onMounted(() => {
|
|
183
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
184
|
+
});
|
|
185
|
+
|
|
70
186
|
onUnmounted(() => {
|
|
71
187
|
window.removeEventListener("resize", handleResize);
|
|
188
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
72
189
|
});
|
|
73
190
|
</script>
|
|
191
|
+
|
|
192
|
+
<style scoped>
|
|
193
|
+
.queue-stats {
|
|
194
|
+
display: grid;
|
|
195
|
+
width: 100%;
|
|
196
|
+
min-width: 0;
|
|
197
|
+
grid-template-columns: repeat(var(--stats-card-count), minmax(0, 1fr));
|
|
198
|
+
gap: 0.35rem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.queue-stats__card {
|
|
202
|
+
@apply bg-dark-400 border-2 border-dark-550;
|
|
203
|
+
height: 2.5rem;
|
|
204
|
+
min-width: 0;
|
|
205
|
+
gap: 0.35rem;
|
|
206
|
+
border-radius: 0.55rem;
|
|
207
|
+
padding: 0 0.5rem;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.queue-stats__header {
|
|
211
|
+
min-width: 1rem;
|
|
212
|
+
flex: 0 0 auto;
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
gap: 0.25rem;
|
|
216
|
+
color: rgb(212 212 219 / 1);
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
font-size: 0.7rem;
|
|
219
|
+
line-height: 1.1;
|
|
220
|
+
white-space: nowrap;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.queue-stats__value {
|
|
224
|
+
min-width: 0;
|
|
225
|
+
flex: 1 1 auto;
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: flex-end;
|
|
229
|
+
overflow: hidden;
|
|
230
|
+
text-align: right;
|
|
231
|
+
text-overflow: clip;
|
|
232
|
+
white-space: nowrap;
|
|
233
|
+
color: rgb(212 212 219 / 1);
|
|
234
|
+
font-weight: 700;
|
|
235
|
+
font-size: 0.7rem;
|
|
236
|
+
font-variant-numeric: tabular-nums;
|
|
237
|
+
line-height: 1.1;
|
|
238
|
+
margin-left: 0.25rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.queue-stats__card--metric .queue-stats__header {
|
|
242
|
+
min-width: 1rem;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.queue-stats__card--metric .queue-stats__value {
|
|
246
|
+
flex: 0 0 auto;
|
|
247
|
+
min-width: 3.4rem;
|
|
248
|
+
margin-left: 0.35rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.queue-stats__header :deep(svg),
|
|
252
|
+
.queue-stats__header img {
|
|
253
|
+
width: 12px;
|
|
254
|
+
height: 12px;
|
|
255
|
+
flex-shrink: 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.queue-stats__card--passes {
|
|
259
|
+
padding-right: 0.4rem;
|
|
260
|
+
gap: 0.3rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.queue-stats__card--passes .queue-stats__header {
|
|
264
|
+
min-width: 1.1rem;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.queue-stats__value--passes {
|
|
268
|
+
font-size: 0.67rem;
|
|
269
|
+
gap: 0.35rem;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.queue-stats__pass-text {
|
|
273
|
+
min-width: 0;
|
|
274
|
+
flex: 1 1 auto;
|
|
275
|
+
overflow: hidden;
|
|
276
|
+
white-space: nowrap;
|
|
277
|
+
text-overflow: ellipsis;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.queue-stats__more-btn {
|
|
281
|
+
@apply bg-dark-550 border border-dark-650 text-light-300;
|
|
282
|
+
flex-shrink: 0;
|
|
283
|
+
border-radius: 9999px;
|
|
284
|
+
padding: 0.1rem 0.4rem;
|
|
285
|
+
font-size: 0.62rem;
|
|
286
|
+
font-weight: 700;
|
|
287
|
+
line-height: 1.2;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.queue-pass-overlay {
|
|
291
|
+
position: fixed;
|
|
292
|
+
inset: 0;
|
|
293
|
+
z-index: 90;
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: center;
|
|
297
|
+
background: rgba(0, 0, 0, 0.45);
|
|
298
|
+
padding: 1rem;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.queue-pass-modal {
|
|
302
|
+
@apply bg-dark-400 border border-dark-550;
|
|
303
|
+
width: min(34rem, 95vw);
|
|
304
|
+
border-radius: 0.75rem;
|
|
305
|
+
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.4);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.queue-pass-modal__header {
|
|
309
|
+
@apply border-b border-dark-550 text-light-200;
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: space-between;
|
|
313
|
+
gap: 0.5rem;
|
|
314
|
+
padding: 0.75rem 0.9rem;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.queue-pass-modal__header h3 {
|
|
318
|
+
font-size: 0.9rem;
|
|
319
|
+
font-weight: 700;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.queue-pass-modal__close {
|
|
323
|
+
@apply bg-dark-500 border border-dark-650 text-light-300;
|
|
324
|
+
border-radius: 0.45rem;
|
|
325
|
+
padding: 0.25rem 0.5rem;
|
|
326
|
+
font-size: 0.72rem;
|
|
327
|
+
font-weight: 600;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.queue-pass-modal__body {
|
|
331
|
+
max-height: min(60vh, 28rem);
|
|
332
|
+
overflow-y: auto;
|
|
333
|
+
padding: 0.55rem;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.queue-pass-modal__list {
|
|
337
|
+
display: grid;
|
|
338
|
+
gap: 0.35rem;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.queue-pass-modal__item {
|
|
342
|
+
@apply bg-dark-500 border border-dark-650 text-light-300;
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
justify-content: space-between;
|
|
346
|
+
border-radius: 0.5rem;
|
|
347
|
+
padding: 0.35rem 0.55rem;
|
|
348
|
+
font-size: 0.78rem;
|
|
349
|
+
font-variant-numeric: tabular-nums;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
@media (min-width: 768px) {
|
|
353
|
+
.queue-stats {
|
|
354
|
+
gap: 0.45rem;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.queue-stats__card {
|
|
358
|
+
padding: 0 0.7rem;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.queue-stats__header {
|
|
362
|
+
gap: 0.35rem;
|
|
363
|
+
font-size: 0.72rem;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.queue-stats__value {
|
|
367
|
+
font-size: 0.72rem;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.queue-stats__value--passes {
|
|
371
|
+
font-size: 0.7rem;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.queue-stats__card--metric .queue-stats__value {
|
|
375
|
+
min-width: 3.7rem;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@media (min-width: 960px) {
|
|
380
|
+
.queue-stats--full {
|
|
381
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.35fr);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
@media (min-width: 1280px) {
|
|
386
|
+
.queue-stats--full {
|
|
387
|
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.queue-stats--full .queue-stats__card--passes {
|
|
391
|
+
grid-column: span 2;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
@media (min-width: 1280px) {
|
|
396
|
+
.queue-stats__value--passes {
|
|
397
|
+
font-size: 0.75rem;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
</style>
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
</div>
|
|
86
86
|
</div>
|
|
87
87
|
<div class="col-span-2 overflow-hidden lg:col-span-3 xl:col-span-2">
|
|
88
|
-
<h4 class="lg:text-2xs text-center text-xs leading-tight
|
|
89
|
-
<span v-if="!props.task.reservedTicketsList">-</span>
|
|
88
|
+
<h4 class="lg:text-2xs text-center text-xs leading-tight">
|
|
89
|
+
<span v-if="!props.task.reservedTicketsList" class="text-light-300">-</span>
|
|
90
90
|
<div v-else class="overflow-hidden">
|
|
91
91
|
<div
|
|
92
92
|
v-for="(l, index) in props.task.reservedTicketsList.split('\n')"
|
|
@@ -99,7 +99,12 @@
|
|
|
99
99
|
'font-bold text-green-400': isTotalPrice(
|
|
100
100
|
l,
|
|
101
101
|
index,
|
|
102
|
-
props.task.reservedTicketsList.split('
|
|
102
|
+
props.task.reservedTicketsList.split('\n')
|
|
103
|
+
),
|
|
104
|
+
'text-light-300': !isTotalPrice(
|
|
105
|
+
l,
|
|
106
|
+
index,
|
|
107
|
+
props.task.reservedTicketsList.split('\n')
|
|
103
108
|
)
|
|
104
109
|
}">
|
|
105
110
|
{{ l.trim() }}
|
|
@@ -109,7 +114,8 @@
|
|
|
109
114
|
<span
|
|
110
115
|
class="mt-1 block text-xs font-bold"
|
|
111
116
|
:class="{
|
|
112
|
-
'text-red-400': taskTimeLeft === '00:00' || taskTimeLeft === 'No Cartholds'
|
|
117
|
+
'text-red-400': taskTimeLeft === '00:00' || taskTimeLeft === 'No Cartholds',
|
|
118
|
+
'text-light-300': taskTimeLeft !== '00:00' && taskTimeLeft !== 'No Cartholds'
|
|
113
119
|
}">
|
|
114
120
|
{{ taskTimeLeft !== "00:00" ? taskTimeLeft : "Expired" }}
|
|
115
121
|
</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
class="table-component relative box-border flex flex-col overflow-x-hidden rounded-lg bg-dark-500 bg-clip-padding shadow-sm">
|
|
3
4
|
<Header class="grid-cols-10 gap-2 text-center lg:grid-cols-12">
|
|
4
5
|
<div class="col-span-1 flex items-center justify-start lg:col-span-2">
|
|
5
6
|
<Checkbox
|
|
@@ -9,26 +10,26 @@
|
|
|
9
10
|
:isHeader="true" />
|
|
10
11
|
<div class="mx-auto hidden items-center lg:flex" @click="ui.toggleSort('eventId')">
|
|
11
12
|
<EventIcon class="lg:mr-3" />
|
|
12
|
-
<h4 class="hidden lg:flex
|
|
13
|
+
<h4 class="hidden text-white lg:flex">Event</h4>
|
|
13
14
|
<DownIcon v-if="ui.sortData.sortBy === 'eventId' && !ui.sortData.reversed" class="ml-1" />
|
|
14
15
|
<UpIcon v-if="ui.sortData.sortBy === 'eventId' && ui.sortData.reversed" class="ml-1" />
|
|
15
16
|
</div>
|
|
16
17
|
</div>
|
|
17
18
|
<div class="col-span-2 flex items-center justify-center lg:col-span-3 xl:col-span-2" v-once>
|
|
18
19
|
<TicketIcon class="mr-0 lg:mr-3" />
|
|
19
|
-
<h4 class="hidden lg:flex
|
|
20
|
+
<h4 class="hidden text-white lg:flex">Tickets</h4>
|
|
20
21
|
</div>
|
|
21
22
|
<div
|
|
22
23
|
class="col-span-6 flex items-center justify-center md:col-span-5 lg:col-span-4 xl:col-span-5"
|
|
23
24
|
@click="ui.toggleSort('status')">
|
|
24
25
|
<StatusIcon class="mr-0 lg:mr-3" />
|
|
25
|
-
<h4 class="hidden lg:flex
|
|
26
|
+
<h4 class="hidden text-white lg:flex">Status</h4>
|
|
26
27
|
<DownIcon v-if="ui.sortData.sortBy === 'status' && !ui.sortData.reversed" class="ml-1" />
|
|
27
28
|
<UpIcon v-if="ui.sortData.sortBy === 'status' && ui.sortData.reversed" class="ml-1" />
|
|
28
29
|
</div>
|
|
29
30
|
<div class="col-span-1 flex items-center justify-end md:col-span-2 md:justify-center lg:col-span-3" v-once>
|
|
30
31
|
<ClickIcon class="mr-0 lg:mr-3" />
|
|
31
|
-
<h4 class="hidden lg:flex
|
|
32
|
+
<h4 class="hidden text-white lg:flex">Actions</h4>
|
|
32
33
|
</div>
|
|
33
34
|
<div class="absolute right-5 top-3.5 hidden items-center xl:flex">
|
|
34
35
|
<h4 class="text-center text-xs text-white">ID</h4>
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
</Header>
|
|
37
38
|
<DynamicScroller
|
|
38
39
|
v-if="virtualTaskItems.length && useVirtualScroller"
|
|
39
|
-
class="hidden-scrollbars
|
|
40
|
+
class="hidden-scrollbars scrollable min-h-0 touch-pan-y overflow-y-auto overflow-x-hidden"
|
|
40
41
|
:style="{ height: maxTableHeight, maxHeight: maxTableHeight }"
|
|
41
42
|
:items="virtualTaskItems"
|
|
42
43
|
:min-item-size="virtualMinItemSize"
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
]"
|
|
57
58
|
:data-index="index">
|
|
58
59
|
<div
|
|
59
|
-
class="shrink-0 border-b border-dark-650
|
|
60
|
+
class="mobile-portrait:min-h-12.5 min-h-14.5 shrink-0 border-b border-dark-650 transition-colors duration-150 ease-in-out hover:!bg-dark-550 has-[.event-details]:min-h-18.75 md:min-h-17.25">
|
|
60
61
|
<Task
|
|
61
62
|
v-if="props.tasks[item.taskId]"
|
|
62
63
|
:task="props.tasks[item.taskId]"
|
|
@@ -68,12 +69,12 @@
|
|
|
68
69
|
</DynamicScroller>
|
|
69
70
|
<div
|
|
70
71
|
v-else-if="virtualTaskItems.length"
|
|
71
|
-
class="hidden-scrollbars
|
|
72
|
+
class="hidden-scrollbars scrollable min-h-0 touch-pan-y overflow-y-auto overflow-x-hidden"
|
|
72
73
|
:style="{ maxHeight: maxTableHeight }">
|
|
73
74
|
<div
|
|
74
75
|
v-for="(item, index) in virtualTaskItems"
|
|
75
76
|
:key="item.taskId"
|
|
76
|
-
class="shrink-0 border-b border-dark-650
|
|
77
|
+
class="mobile-portrait:min-h-12.5 min-h-14.5 shrink-0 border-b border-dark-650 transition-colors duration-150 ease-in-out hover:!bg-dark-550 has-[.event-details]:min-h-18.75 md:min-h-17.25">
|
|
77
78
|
<Task
|
|
78
79
|
v-if="props.tasks[item.taskId]"
|
|
79
80
|
:task="props.tasks[item.taskId]"
|
|
@@ -83,18 +84,15 @@
|
|
|
83
84
|
</div>
|
|
84
85
|
<div
|
|
85
86
|
v-else
|
|
86
|
-
class="empty-state flex flex-col items-center justify-center py-8 text-center
|
|
87
|
+
class="empty-state flex flex-col items-center justify-center bg-dark-400 py-8 text-center text-sm font-medium text-light-500"
|
|
87
88
|
:style="{ minHeight: emptyStateHeight }">
|
|
88
|
-
<div
|
|
89
|
-
|
|
90
|
-
!ui.queueStats.queued && !ui.queueStats.sleeping && ui.queueStats.nextQueuePasses.length === 0
|
|
91
|
-
">
|
|
92
|
-
<TasksIcon class="mx-auto empty-state-icon" />
|
|
89
|
+
<div v-if="!ui.queueStats.queued && !ui.queueStats.sleeping && ui.queueStats.nextQueuePasses.length === 0">
|
|
90
|
+
<TasksIcon class="empty-state-icon mx-auto" />
|
|
93
91
|
<p class="text-sm text-light-400">No tasks yet</p>
|
|
94
92
|
<p class="mt-1 text-xs text-light-500">Create tasks to get started</p>
|
|
95
93
|
</div>
|
|
96
94
|
<div v-else>
|
|
97
|
-
<TasksIcon class="
|
|
95
|
+
<TasksIcon class="empty-state-icon mx-auto" />
|
|
98
96
|
<p class="text-sm text-light-400">No tasks match current filters</p>
|
|
99
97
|
<p class="mt-1 text-xs text-light-500">Adjust filters to see tasks</p>
|
|
100
98
|
</div>
|
|
@@ -118,7 +116,7 @@ const props = defineProps({
|
|
|
118
116
|
},
|
|
119
117
|
searchQuery: {
|
|
120
118
|
type: String,
|
|
121
|
-
default:
|
|
119
|
+
default: ""
|
|
122
120
|
},
|
|
123
121
|
preferEventName: {
|
|
124
122
|
type: Boolean,
|
|
@@ -203,7 +201,11 @@ const filteredTaskIds = computed(() => {
|
|
|
203
201
|
continue;
|
|
204
202
|
}
|
|
205
203
|
|
|
206
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
task.siteId !== ui.currentCountry.siteId &&
|
|
206
|
+
!siteIdEdgeCases[task.siteId]?.includes(ui.currentCountry.siteId)
|
|
207
|
+
)
|
|
208
|
+
continue;
|
|
207
209
|
if (ui.currentEvent && task.eventId !== ui.currentEvent) continue;
|
|
208
210
|
if (!shouldTaskShow(task)) continue;
|
|
209
211
|
if (!shouldTaskMatchSearch(task, searchLower)) continue;
|
|
@@ -264,33 +266,45 @@ const handleVirtualWheel = (event) => {
|
|
|
264
266
|
|
|
265
267
|
const maxTableHeight = computed(() => {
|
|
266
268
|
// Detect PWA mode (standalone display)
|
|
267
|
-
const isPWA = window.matchMedia(
|
|
269
|
+
const isPWA = window.matchMedia("(display-mode: standalone)").matches;
|
|
268
270
|
|
|
269
271
|
// Calculate available space for table
|
|
270
272
|
const headerHeight = windowWidth.value >= 1024 ? 80 : 64; // Navbar padding (lg:pt-20 vs pt-16)
|
|
271
273
|
const titleHeight = 45; // Tasks title and mobile controls
|
|
272
274
|
const controlsHeight = windowWidth.value >= 650 ? 55 : 0; // Desktop controls
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
275
|
+
// Keep this in sync with Tasks.vue controls layout:
|
|
276
|
+
// - >= 1280px: stats stays in same row as filters
|
|
277
|
+
// - 768px..1279px: stats takes its own wrapped row when visible
|
|
278
|
+
// - < 768px: mobile has dedicated stats row + controls/search rows
|
|
279
|
+
const isDesktopInlineStats = windowWidth.value >= 1280;
|
|
280
|
+
const isTabletWrappedStats = windowWidth.value >= 768 && windowWidth.value < 1280;
|
|
281
|
+
|
|
282
|
+
const filtersAndStatsHeight = (() => {
|
|
283
|
+
if (isDesktopInlineStats) {
|
|
284
|
+
return ui.queueStats.show ? 50 : 45;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (isTabletWrappedStats) {
|
|
288
|
+
const filterRowHeight = 45;
|
|
289
|
+
const wrappedStatsRowHeight = ui.queueStats.show ? 54 : 0;
|
|
290
|
+
return filterRowHeight + wrappedStatsRowHeight;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return ui.queueStats.show ? 155 : 110;
|
|
294
|
+
})();
|
|
283
295
|
|
|
284
296
|
// Reserve more space for UTILS on iPhone PWA to prevent overflow
|
|
285
|
-
const utilitiesHeight =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
const utilitiesHeight =
|
|
298
|
+
isPWA && windowWidth.value <= 768
|
|
299
|
+
? 150 // iPhone PWA: extra space for UTILS
|
|
300
|
+
: windowWidth.value > 1024
|
|
301
|
+
? 200 // Desktop
|
|
302
|
+
: windowWidth.value <= 768
|
|
303
|
+
? 150 // Mobile browser
|
|
304
|
+
: 210; // Tablet
|
|
305
|
+
|
|
306
|
+
const margins =
|
|
307
|
+
windowWidth.value >= 1024 ? 30 : windowWidth.value <= 480 && windowHeight.value > windowWidth.value ? 8 : 12;
|
|
294
308
|
|
|
295
309
|
const totalUsedSpace =
|
|
296
310
|
headerHeight + titleHeight + controlsHeight + filtersAndStatsHeight + utilitiesHeight + margins;
|
package/src/libs/Filter.js
CHANGED
|
@@ -164,7 +164,6 @@ const getSectionNameMapping = () => {
|
|
|
164
164
|
}
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
-
log("getSectionNameMapping:", sectionRealName);
|
|
168
167
|
return sectionRealName;
|
|
169
168
|
};
|
|
170
169
|
|
|
@@ -583,14 +582,11 @@ export default class FilterBuilder {
|
|
|
583
582
|
// Use BOTH stroke and fill to cover entire section area, not just row lines
|
|
584
583
|
this.cssClasses += `.svg-wrapper path.hover-highlight-stroke {stroke: ${colors.HIGHLIGHT} !important; fill: ${colors.HIGHLIGHT} !important; fill-opacity: 0.3 !important;}\n`;
|
|
585
584
|
this.cssClasses += `.svg-wrapper path.hover-highlight-fill {fill: ${colors.HIGHLIGHT} !important;}\n`;
|
|
586
|
-
|
|
587
|
-
log("Generated CSS:", this.cssClasses);
|
|
588
585
|
}
|
|
589
586
|
|
|
590
587
|
addLabelHandlers() {
|
|
591
588
|
const labels = document.querySelectorAll("tspan");
|
|
592
589
|
const gaSectionNameMapping = this.getSectionNameMapping();
|
|
593
|
-
log("addLabelHandlers: gaSectionNameMapping", gaSectionNameMapping);
|
|
594
590
|
|
|
595
591
|
for (let i = 0; i < labels.length; i++) {
|
|
596
592
|
const label = labels[i];
|
package/src/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createApp } from "vue";
|
|
2
2
|
import { createPinia } from "pinia";
|
|
3
3
|
import VueVirtualScroller from "vue-virtual-scroller";
|
|
4
|
+
import Vue3Toastify from "vue3-toastify";
|
|
4
5
|
|
|
5
6
|
import App from "./App.vue";
|
|
6
7
|
import router from "./router";
|
|
@@ -18,4 +19,8 @@ app.config.performance = true;
|
|
|
18
19
|
app.use(createPinia());
|
|
19
20
|
app.use(router);
|
|
20
21
|
app.use(VueVirtualScroller);
|
|
22
|
+
app.use(Vue3Toastify, {
|
|
23
|
+
newestOnTop: true,
|
|
24
|
+
position: "top-right"
|
|
25
|
+
});
|
|
21
26
|
app.mount("#app");
|
package/src/stores/connection.js
CHANGED
|
@@ -3,6 +3,7 @@ import WebsocketHeartbeatJs from "websocket-heartbeat-js";
|
|
|
3
3
|
import { decode } from "@msgpack/msgpack";
|
|
4
4
|
import router from "@/router/index";
|
|
5
5
|
import { sortTaskIds } from "@/libs/utils/array";
|
|
6
|
+
import { DEBUG } from "@/utils/debug";
|
|
6
7
|
|
|
7
8
|
const TASK_UPDATE_EVENTS = new Set(["task-update", "task-setstatus", "task-setinfo"]);
|
|
8
9
|
const QUEUE_STAT_FIELDS = new Set(["siteId", "eventId", "inQueue", "status", "queuePosition"]);
|
|
@@ -568,6 +569,7 @@ export class ConnectionHandler {
|
|
|
568
569
|
if (msg?.data) msg.data.module = this.ui.currentModule;
|
|
569
570
|
this.socket.send(JSON.stringify(msg));
|
|
570
571
|
} catch {
|
|
572
|
+
if (DEBUG) return;
|
|
571
573
|
this.ui.startSpinner("Reconnecting");
|
|
572
574
|
}
|
|
573
575
|
}
|
|
@@ -604,13 +606,13 @@ export class ConnectionHandler {
|
|
|
604
606
|
};
|
|
605
607
|
|
|
606
608
|
this.socket.onclose = () => {
|
|
607
|
-
this.ui.startSpinner("Reconnecting");
|
|
609
|
+
if (!DEBUG) this.ui.startSpinner("Reconnecting");
|
|
608
610
|
hasShownDisconnect = true;
|
|
609
611
|
this.clearTaskUpdateQueue();
|
|
610
612
|
};
|
|
611
613
|
|
|
612
614
|
this.socket.onerror = () => {
|
|
613
|
-
this.ui.startSpinner("Reconnecting");
|
|
615
|
+
if (!DEBUG) this.ui.startSpinner("Reconnecting");
|
|
614
616
|
hasShownDisconnect = true;
|
|
615
617
|
this.clearTaskUpdateQueue();
|
|
616
618
|
};
|
package/src/stores/ui.js
CHANGED
|
@@ -5,6 +5,10 @@ import { toast } from "vue3-toastify";
|
|
|
5
5
|
import { createLogger } from "@/stores/logger";
|
|
6
6
|
import { betterSort, sortTaskIds } from "@/libs/utils/array";
|
|
7
7
|
|
|
8
|
+
// Track active toast IDs for manual limit enforcement
|
|
9
|
+
const activeToasts = [];
|
|
10
|
+
const MAX_TOASTS = 2;
|
|
11
|
+
|
|
8
12
|
const TOAST_CONFIG = {
|
|
9
13
|
autoClose: 1400,
|
|
10
14
|
pauseOnHover: false,
|
|
@@ -17,6 +21,27 @@ const TOAST_CONFIG = {
|
|
|
17
21
|
newestOnTop: true
|
|
18
22
|
};
|
|
19
23
|
|
|
24
|
+
// Helper to enforce toast limit
|
|
25
|
+
const showToastWithLimit = (type, message) => {
|
|
26
|
+
// If we're at the limit, remove the oldest toast
|
|
27
|
+
if (activeToasts.length >= MAX_TOASTS) {
|
|
28
|
+
const oldestId = activeToasts.shift();
|
|
29
|
+
toast.remove(oldestId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Show new toast and track its ID
|
|
33
|
+
const toastId = toast[type](message, TOAST_CONFIG);
|
|
34
|
+
activeToasts.push(toastId);
|
|
35
|
+
|
|
36
|
+
// Remove from tracking when it auto-closes
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
const index = activeToasts.indexOf(toastId);
|
|
39
|
+
if (index > -1) activeToasts.splice(index, 1);
|
|
40
|
+
}, TOAST_CONFIG.autoClose + 100);
|
|
41
|
+
|
|
42
|
+
return toastId;
|
|
43
|
+
};
|
|
44
|
+
|
|
20
45
|
import mockTaskData from "@/stores/sampleData.js";
|
|
21
46
|
import { useRouter } from "vue-router";
|
|
22
47
|
import { DEBUG } from "@/utils/debug";
|
|
@@ -24,6 +49,16 @@ import { DEBUG } from "@/utils/debug";
|
|
|
24
49
|
const ALL_SELECTED = 0;
|
|
25
50
|
const SOME_SELECTED = 1;
|
|
26
51
|
const NO_SELECTED = 2;
|
|
52
|
+
const DEV_QUEUE_STATS_STORAGE_KEY = "dev-queue-stats-mock";
|
|
53
|
+
const DEV_QUEUE_STATS_PRESETS = {
|
|
54
|
+
heavy: {
|
|
55
|
+
show: true,
|
|
56
|
+
total: 10000,
|
|
57
|
+
queued: 5000,
|
|
58
|
+
sleeping: 3000,
|
|
59
|
+
nextQueuePasses: [11, 18, 26, 35, 49, 57, 72, 88, 103, 121, 147, 169]
|
|
60
|
+
}
|
|
61
|
+
};
|
|
27
62
|
|
|
28
63
|
export const useUIStore = defineStore("ui", () => {
|
|
29
64
|
const logger = createLogger("UI");
|
|
@@ -98,6 +133,55 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
98
133
|
sleeping: 0,
|
|
99
134
|
nextQueuePasses: []
|
|
100
135
|
});
|
|
136
|
+
const devQueueStatsMock = ref(null);
|
|
137
|
+
|
|
138
|
+
const normalizeQueueStats = (input = {}) => {
|
|
139
|
+
const total = Number(input.total) || 0;
|
|
140
|
+
const queued = Number(input.queued) || 0;
|
|
141
|
+
const sleeping = Number(input.sleeping) || 0;
|
|
142
|
+
const nextQueuePasses = Array.isArray(input.nextQueuePasses)
|
|
143
|
+
? input.nextQueuePasses.map((value) => Number(value)).filter((value) => Number.isFinite(value) && value > 0)
|
|
144
|
+
: [];
|
|
145
|
+
const show = Boolean(input.show ?? (queued > 0 || sleeping > 0 || nextQueuePasses.length > 0));
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
show,
|
|
149
|
+
total,
|
|
150
|
+
queued,
|
|
151
|
+
sleeping,
|
|
152
|
+
nextQueuePasses: nextQueuePasses.sort((a, b) => a - b)
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const applyQueueStats = (stats) => {
|
|
157
|
+
queueStats.value.show = stats.show;
|
|
158
|
+
queueStats.value.total = stats.total;
|
|
159
|
+
queueStats.value.queued = stats.queued;
|
|
160
|
+
queueStats.value.sleeping = stats.sleeping;
|
|
161
|
+
queueStats.value.nextQueuePasses = stats.nextQueuePasses;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const setDevQueueStatsMock = (stats) => {
|
|
165
|
+
if (!DEBUG) return;
|
|
166
|
+
if (!stats) {
|
|
167
|
+
devQueueStatsMock.value = null;
|
|
168
|
+
localStorage.removeItem(DEV_QUEUE_STATS_STORAGE_KEY);
|
|
169
|
+
refreshQueueStats();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const normalized = normalizeQueueStats(stats);
|
|
174
|
+
devQueueStatsMock.value = normalized;
|
|
175
|
+
localStorage.setItem(DEV_QUEUE_STATS_STORAGE_KEY, JSON.stringify(normalized));
|
|
176
|
+
applyQueueStats(normalized);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const setDevQueueStatsPreset = (preset = "heavy") => {
|
|
180
|
+
if (!DEBUG) return;
|
|
181
|
+
const data = DEV_QUEUE_STATS_PRESETS[preset];
|
|
182
|
+
if (!data) return;
|
|
183
|
+
setDevQueueStatsMock(data);
|
|
184
|
+
};
|
|
101
185
|
|
|
102
186
|
const router = useRouter();
|
|
103
187
|
const openContextMenu = ref("");
|
|
@@ -205,7 +289,6 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
205
289
|
input.style.userSelect = "auto";
|
|
206
290
|
input.style.touchAction = "pan-x pan-y";
|
|
207
291
|
});
|
|
208
|
-
|
|
209
292
|
};
|
|
210
293
|
|
|
211
294
|
const enableScroll = () => {
|
|
@@ -242,10 +325,14 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
242
325
|
|
|
243
326
|
// Restore scroll position
|
|
244
327
|
window.scrollTo(0, parseInt(scrollY || "0") * -1);
|
|
245
|
-
|
|
246
328
|
};
|
|
247
329
|
|
|
248
330
|
const refreshQueueStats = () => {
|
|
331
|
+
if (DEBUG && devQueueStatsMock.value) {
|
|
332
|
+
applyQueueStats(devQueueStatsMock.value);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
249
336
|
const siteId = currentCountry.value.siteId;
|
|
250
337
|
const eventId = currentEvent.value;
|
|
251
338
|
const sleepingStatuses = new Set([
|
|
@@ -284,6 +371,23 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
284
371
|
queueStats.value.show = queued > 0 || sleeping > 0 || positions.length > 0;
|
|
285
372
|
};
|
|
286
373
|
|
|
374
|
+
if (DEBUG) {
|
|
375
|
+
try {
|
|
376
|
+
const saved = localStorage.getItem(DEV_QUEUE_STATS_STORAGE_KEY);
|
|
377
|
+
if (saved) {
|
|
378
|
+
const parsed = JSON.parse(saved);
|
|
379
|
+
devQueueStatsMock.value = normalizeQueueStats(parsed);
|
|
380
|
+
applyQueueStats(devQueueStatsMock.value);
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
localStorage.removeItem(DEV_QUEUE_STATS_STORAGE_KEY);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
window.__setQueueStatsMock = (stats) => setDevQueueStatsMock(stats);
|
|
387
|
+
window.__setQueueStatsPreset = (preset = "heavy") => setDevQueueStatsPreset(preset);
|
|
388
|
+
window.__clearQueueStatsMock = () => setDevQueueStatsMock(null);
|
|
389
|
+
}
|
|
390
|
+
|
|
287
391
|
const toggleModal = (name, clearValue = false) => {
|
|
288
392
|
if (disabledButtons.value["add-tasks"] && name === "create-task") return;
|
|
289
393
|
if (clearValue) currentlyEditing.value = {};
|
|
@@ -457,7 +561,6 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
457
561
|
mainCheckbox,
|
|
458
562
|
toggleMainCheckbox,
|
|
459
563
|
|
|
460
|
-
|
|
461
564
|
// single
|
|
462
565
|
deleteTask: (taskId) => {
|
|
463
566
|
delete tasks.value[taskId];
|
|
@@ -526,8 +629,8 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
526
629
|
massEditPresaleCode: (eventId, presaleCode) => connection.sendMassEditPresaleCode(eventId, presaleCode),
|
|
527
630
|
|
|
528
631
|
// alerts
|
|
529
|
-
showError: (err) =>
|
|
530
|
-
showSuccess: (msg) =>
|
|
632
|
+
showError: (err) => showToastWithLimit("error", err),
|
|
633
|
+
showSuccess: (msg) => showToastWithLimit("success", msg),
|
|
531
634
|
// Country
|
|
532
635
|
currentCountry,
|
|
533
636
|
setCurrentCountry: (country, closeModal, newModule) => {
|
|
@@ -551,6 +654,8 @@ export const useUIStore = defineStore("ui", () => {
|
|
|
551
654
|
|
|
552
655
|
disabledButtons,
|
|
553
656
|
queueStats,
|
|
657
|
+
setDevQueueStatsMock,
|
|
658
|
+
setDevQueueStatsPreset,
|
|
554
659
|
|
|
555
660
|
refreshQueueStats,
|
|
556
661
|
|
package/src/views/Tasks.vue
CHANGED
|
@@ -37,24 +37,11 @@
|
|
|
37
37
|
@deleteAll="ui.deleteTasks()" />
|
|
38
38
|
</div>
|
|
39
39
|
|
|
40
|
-
<div class="mb-2
|
|
41
|
-
<Stats class="stats-component
|
|
42
|
-
<div class="flex-gap-2 items-center">
|
|
43
|
-
<div
|
|
44
|
-
class="flex h-8 items-center justify-between rounded-md border border-dark-650 bg-dark-400 px-2 text-xs font-medium text-white">
|
|
45
|
-
<p class="text-2xs">Name</p>
|
|
46
|
-
<Switch class="scale-75" v-model="preferEventName" />
|
|
47
|
-
</div>
|
|
48
|
-
<PriceSortToggle
|
|
49
|
-
class="h-8 min-w-20 max-w-28 flex-shrink-0"
|
|
50
|
-
:options="['All', 'Checkout']"
|
|
51
|
-
:darker="true"
|
|
52
|
-
:current="ui.taskFilter"
|
|
53
|
-
@change="(e) => ui.setTaskFilter(e)" />
|
|
54
|
-
</div>
|
|
40
|
+
<div class="mb-2 md:hidden">
|
|
41
|
+
<Stats class="stats-component w-full" />
|
|
55
42
|
</div>
|
|
56
43
|
|
|
57
|
-
<div class="mb-1 hidden items-center gap-2 md:flex lg:mb-2">
|
|
44
|
+
<div class="mb-1 hidden flex-wrap items-center gap-2 md:flex xl:flex-nowrap lg:mb-2">
|
|
58
45
|
<div v-if="uniqEventIds.length > 1" class="w-52 flex-shrink-0">
|
|
59
46
|
<Dropdown
|
|
60
47
|
:onClick="(f) => ui.setCurrentEvent(f)"
|
|
@@ -87,7 +74,9 @@
|
|
|
87
74
|
:darker="true"
|
|
88
75
|
:current="ui.taskFilter"
|
|
89
76
|
@change="(e) => ui.setTaskFilter(e)" />
|
|
90
|
-
<
|
|
77
|
+
<div class="stats-component min-w-0 w-full md:basis-full xl:ml-auto xl:basis-auto xl:flex-1 xl:max-w-2xl">
|
|
78
|
+
<Stats class="w-full" />
|
|
79
|
+
</div>
|
|
91
80
|
</div>
|
|
92
81
|
|
|
93
82
|
<div class="mb-1 flex flex-col gap-2 md:hidden">
|
|
@@ -101,16 +90,29 @@
|
|
|
101
90
|
class="input-default event-dropdown w-full hover:bg-dark-400"
|
|
102
91
|
rightAmount="right-2" />
|
|
103
92
|
</div>
|
|
104
|
-
<div class="w-full">
|
|
105
|
-
<div class="
|
|
106
|
-
<input
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
93
|
+
<div class="flex w-full items-center gap-2">
|
|
94
|
+
<div class="flex-1">
|
|
95
|
+
<div class="input-default flex items-center">
|
|
96
|
+
<input
|
|
97
|
+
v-model="taskSearchQuery"
|
|
98
|
+
type="text"
|
|
99
|
+
placeholder="Search tasks..."
|
|
100
|
+
aria-label="Search tasks"
|
|
101
|
+
class="transparent-input" />
|
|
102
|
+
<span v-if="taskSearchQuery" class="ml-2 text-xs text-light-500">{{ filteredTaskCount }}</span>
|
|
103
|
+
</div>
|
|
113
104
|
</div>
|
|
105
|
+
<div
|
|
106
|
+
class="flex h-8 items-center justify-between rounded-md border border-dark-650 bg-dark-400 px-2 text-xs font-medium text-white flex-shrink-0">
|
|
107
|
+
<p class="text-2xs">Name</p>
|
|
108
|
+
<Switch class="scale-75" v-model="preferEventName" />
|
|
109
|
+
</div>
|
|
110
|
+
<PriceSortToggle
|
|
111
|
+
class="h-8 min-w-20 max-w-28 flex-shrink-0"
|
|
112
|
+
:options="['All', 'Checkout']"
|
|
113
|
+
:darker="true"
|
|
114
|
+
:current="ui.taskFilter"
|
|
115
|
+
@change="(e) => ui.setTaskFilter(e)" />
|
|
114
116
|
</div>
|
|
115
117
|
</div>
|
|
116
118
|
|