@necrolab/dashboard 0.5.28 → 0.5.29
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/backend/api.js +7 -5
- package/backend/batching.js +59 -2
- package/backend/index.js +1 -1
- package/index.html +10 -23
- package/package.json +1 -1
- package/src/assets/css/base/scroll.scss +1 -1
- package/src/assets/css/main.scss +14 -14
- package/src/components/Console/ConsoleToolbar.vue +8 -8
- package/src/components/Editors/Account/Account.vue +9 -5
- package/src/components/Editors/Account/AccountView.vue +37 -18
- package/src/components/Editors/Account/CreateAccount.vue +38 -4
- package/src/components/Editors/Profile/CreateProfile.vue +29 -4
- package/src/components/Editors/Profile/Profile.vue +11 -6
- package/src/components/Editors/Profile/ProfileCountryChooser.vue +2 -2
- package/src/components/Editors/Profile/ProfileView.vue +37 -18
- package/src/components/Tasks/CreateTaskAXS.vue +16 -2
- package/src/components/Tasks/CreateTaskTM.vue +28 -5
- package/src/components/Tasks/QuickSettings.vue +77 -10
- package/src/components/Tasks/Task.vue +20 -7
- package/src/components/Tasks/TaskView.vue +144 -58
- package/src/components/Tasks/ViewTask.vue +17 -3
- package/src/components/ui/Modal.vue +1 -1
- package/src/components/ui/ReadonlyFieldsSection.vue +3 -3
- package/src/components/ui/StatusBadge.vue +1 -1
- package/src/components/ui/TaskToggle.vue +2 -3
- package/src/components/ui/controls/CountryChooser.vue +2 -2
- package/src/components/ui/controls/atomic/Dropdown.vue +2 -2
- package/src/components/ui/controls/atomic/MultiDropdown.vue +1 -1
- package/src/composables/useDynamicTableHeight.js +4 -4
- package/src/composables/useRowSelection.js +0 -1
- package/src/composables/useZoomPrevention.js +16 -55
- package/src/stores/connection.js +453 -68
- package/src/stores/sampleData.js +34 -24
- package/src/stores/ui.js +89 -100
- package/src/views/Accounts.vue +2 -5
- package/src/views/Console.vue +13 -14
- package/src/views/Profiles.vue +2 -5
- package/vite.config.js +4 -2
|
@@ -33,19 +33,23 @@
|
|
|
33
33
|
<h4 class="hidden text-white md:flex md:ml-3">Actions</h4>
|
|
34
34
|
</div>
|
|
35
35
|
</Header>
|
|
36
|
-
<
|
|
37
|
-
v-if="
|
|
38
|
-
class="hidden-scrollbars
|
|
39
|
-
:style="{ maxHeight: dynamicTableHeight }"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
<RecycleScroller
|
|
37
|
+
v-if="props.profiles.length !== 0"
|
|
38
|
+
class="hidden-scrollbars touch-pan-y overflow-y-auto overflow-x-hidden transition-colors duration-150 table-scroll scrollable"
|
|
39
|
+
:style="{ height: dynamicTableHeight, maxHeight: dynamicTableHeight }"
|
|
40
|
+
:items="virtualProfiles"
|
|
41
|
+
key-field="virtualKey"
|
|
42
|
+
:item-size="64"
|
|
43
|
+
@wheel.passive="handleVirtualWheel">
|
|
44
|
+
<template #default="{ item, index }">
|
|
45
|
+
<div class="min-h-16 flex-shrink-0 hover:bg-dark-550">
|
|
46
|
+
<Profile
|
|
47
|
+
:class="getRowClass(index)"
|
|
48
|
+
:profile="item.profile"
|
|
49
|
+
:privacy="props.privacy" />
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
</RecycleScroller>
|
|
49
53
|
<EmptyState v-else :icon="ProfileIcon" message="No profiles found" subtitle="Create profiles to get started" />
|
|
50
54
|
</div>
|
|
51
55
|
</template>
|
|
@@ -57,22 +61,37 @@ import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
|
|
|
57
61
|
import EmptyState from "@/components/ui/EmptyState.vue";
|
|
58
62
|
import { useUIStore } from "@/stores/ui";
|
|
59
63
|
import { useDynamicTableHeight } from "@/composables/useDynamicTableHeight";
|
|
60
|
-
import { computed } from "vue";
|
|
61
|
-
import { useTableRender } from "@/composables/useTableRender";
|
|
62
64
|
import { getRowClass } from "@/utils/tableHelpers";
|
|
65
|
+
import { RecycleScroller } from "vue-virtual-scroller";
|
|
66
|
+
import { computed } from "vue";
|
|
63
67
|
|
|
64
68
|
const props = defineProps({
|
|
65
69
|
profiles: {
|
|
66
|
-
type:
|
|
70
|
+
type: Array,
|
|
67
71
|
required: true
|
|
72
|
+
},
|
|
73
|
+
privacy: {
|
|
74
|
+
type: Boolean,
|
|
75
|
+
default: true
|
|
68
76
|
}
|
|
69
77
|
});
|
|
70
78
|
|
|
71
79
|
const ui = useUIStore();
|
|
72
80
|
|
|
73
|
-
const { toRender } = useTableRender(computed(() => props.profiles));
|
|
74
|
-
|
|
75
81
|
import { TABLE_LAYOUT } from "@/constants/tableLayout";
|
|
76
82
|
|
|
77
83
|
const { dynamicTableHeight } = useDynamicTableHeight(TABLE_LAYOUT.PROFILES);
|
|
84
|
+
|
|
85
|
+
const virtualProfiles = computed(() =>
|
|
86
|
+
props.profiles.map((profile) => ({
|
|
87
|
+
profile,
|
|
88
|
+
virtualKey: String(profile.id ?? `${profile.profileName ?? "profile"}-${profile.cardNumber ?? ""}`)
|
|
89
|
+
}))
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const handleVirtualWheel = (event) => {
|
|
93
|
+
const target = event.currentTarget;
|
|
94
|
+
if (!target) return;
|
|
95
|
+
target.scrollTop += event.deltaY;
|
|
96
|
+
};
|
|
78
97
|
</script>
|
|
@@ -108,6 +108,7 @@ import {
|
|
|
108
108
|
StadiumIcon,
|
|
109
109
|
ScannerIcon,
|
|
110
110
|
BagIcon,
|
|
111
|
+
HashIcon,
|
|
111
112
|
TagIcon,
|
|
112
113
|
SkiIcon,
|
|
113
114
|
HandIcon,
|
|
@@ -131,7 +132,7 @@ const defaultTags = ["Amex", "Visa", "Master"];
|
|
|
131
132
|
const profileTagsOptions = ref(
|
|
132
133
|
removeDuplicates(["Any", ...defaultTags, ...ui.profile.profileTags.map((x) => firstUpper(x))])
|
|
133
134
|
);
|
|
134
|
-
const
|
|
135
|
+
const createBaseTask = () => ({
|
|
135
136
|
selected: false,
|
|
136
137
|
taskId: "",
|
|
137
138
|
active: false,
|
|
@@ -153,7 +154,20 @@ const baseTask = ref({
|
|
|
153
154
|
promoId: ""
|
|
154
155
|
});
|
|
155
156
|
|
|
156
|
-
const
|
|
157
|
+
const booleanTaskFields = ["manual", "doNotPay", "quickQueue", "smartTimer"];
|
|
158
|
+
|
|
159
|
+
const sanitizeTaskState = (savedState = {}) => {
|
|
160
|
+
const defaults = createBaseTask();
|
|
161
|
+
const merged = { ...defaults, ...savedState };
|
|
162
|
+
|
|
163
|
+
for (const field of booleanTaskFields) {
|
|
164
|
+
merged[field] = typeof merged[field] === "boolean" ? merged[field] : defaults[field];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return merged;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const task = ref(sanitizeTaskState(ui.modalData[`task_${ui.currentCountry.siteId}`]));
|
|
157
171
|
|
|
158
172
|
function createTask() {
|
|
159
173
|
ui.logger.Info("Created new task", task.value.taskId);
|
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
StadiumIcon,
|
|
137
137
|
ScannerIcon,
|
|
138
138
|
BagIcon,
|
|
139
|
+
HashIcon,
|
|
139
140
|
TagIcon,
|
|
140
141
|
SkiIcon,
|
|
141
142
|
HandIcon,
|
|
@@ -164,7 +165,7 @@ const profileTagsOptions = ref(
|
|
|
164
165
|
removeDuplicates(["Any", ...defaultTags, ...ui.profile.profileTags.map((x) => firstUpper(x))])
|
|
165
166
|
);
|
|
166
167
|
|
|
167
|
-
const
|
|
168
|
+
const createBaseTask = () => ({
|
|
168
169
|
selected: false,
|
|
169
170
|
taskId: "",
|
|
170
171
|
active: false,
|
|
@@ -179,10 +180,12 @@ const baseTask = ref({
|
|
|
179
180
|
manual: true,
|
|
180
181
|
doNotPay: false,
|
|
181
182
|
quickQueue: false,
|
|
182
|
-
loginAfterCart:
|
|
183
|
+
loginAfterCart: isEU(ui.currentCountry.siteId),
|
|
183
184
|
smartTimer: false,
|
|
184
185
|
presaleMode: false,
|
|
186
|
+
presaleStrict: false,
|
|
185
187
|
agedAccount: false,
|
|
188
|
+
otpAccount: false,
|
|
186
189
|
accountTag: accountTagOptions.value[0],
|
|
187
190
|
profileTags: ["Any"],
|
|
188
191
|
clOrigin: undefined,
|
|
@@ -190,9 +193,30 @@ const baseTask = ref({
|
|
|
190
193
|
taskQuantity: 1
|
|
191
194
|
});
|
|
192
195
|
|
|
193
|
-
|
|
196
|
+
const booleanTaskFields = [
|
|
197
|
+
"manual",
|
|
198
|
+
"doNotPay",
|
|
199
|
+
"quickQueue",
|
|
200
|
+
"loginAfterCart",
|
|
201
|
+
"smartTimer",
|
|
202
|
+
"presaleMode",
|
|
203
|
+
"presaleStrict",
|
|
204
|
+
"agedAccount",
|
|
205
|
+
"otpAccount"
|
|
206
|
+
];
|
|
194
207
|
|
|
195
|
-
const
|
|
208
|
+
const sanitizeTaskState = (savedState = {}) => {
|
|
209
|
+
const defaults = createBaseTask();
|
|
210
|
+
const merged = { ...defaults, ...savedState };
|
|
211
|
+
|
|
212
|
+
for (const field of booleanTaskFields) {
|
|
213
|
+
merged[field] = typeof merged[field] === "boolean" ? merged[field] : defaults[field];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return merged;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const task = ref(sanitizeTaskState(ui.modalData[`task_${ui.currentCountry.siteId}`]));
|
|
196
220
|
|
|
197
221
|
function createTask() {
|
|
198
222
|
ui.logger.Info("Created new task", task.value.taskId);
|
|
@@ -251,4 +275,3 @@ watch(
|
|
|
251
275
|
}
|
|
252
276
|
);
|
|
253
277
|
</script>
|
|
254
|
-
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<div class="w-2/3 h-full relative">
|
|
16
16
|
<input
|
|
17
17
|
type="text"
|
|
18
|
-
class="api-key-input
|
|
18
|
+
class="api-key-input"
|
|
19
19
|
:placeholder="`Enter ${keyName} key`"
|
|
20
20
|
v-model="quickConfig.keys[categoryName][keyName]"
|
|
21
21
|
/>
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
</div>
|
|
34
34
|
<div v-else-if="balanceState[categoryName]?.[keyName]?.balance !== null && balanceState[categoryName]?.[keyName]?.balance !== undefined" class="flex-gap-1 items-center text-green-400" :title="`Balance: ${balanceState[categoryName]?.[keyName]?.balance}`">
|
|
35
35
|
<img src="@/assets/img/sell.svg" width="9" class="opacity-60" />
|
|
36
|
-
<span class="text-2xs font-bold tabular-nums">{{ formatBalance(balanceState[categoryName]?.[keyName]?.balance) }}</span>
|
|
36
|
+
<span class="text-2xs font-bold tabular-nums balance-value">{{ formatBalance(balanceState[categoryName]?.[keyName]?.balance) }}</span>
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
39
39
|
</div>
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
|
|
155
155
|
.api-key-input {
|
|
156
156
|
@apply w-full h-full text-sm text-white border-0 rounded-md;
|
|
157
|
-
@apply px-3 py-2;
|
|
157
|
+
@apply px-3 py-2 pr-24;
|
|
158
158
|
@apply focus:outline-none;
|
|
159
159
|
background: oklch(26% 0 68deg);
|
|
160
160
|
|
|
@@ -167,6 +167,13 @@
|
|
|
167
167
|
@apply absolute right-3 top-1/2 -translate-y-1/2;
|
|
168
168
|
@apply flex items-center justify-center;
|
|
169
169
|
@apply pointer-events-none;
|
|
170
|
+
max-width: 5.25rem;
|
|
171
|
+
min-width: 1rem;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.balance-value {
|
|
175
|
+
@apply truncate inline-block align-middle;
|
|
176
|
+
max-width: 4.25rem;
|
|
170
177
|
}
|
|
171
178
|
</style>
|
|
172
179
|
<script setup>
|
|
@@ -245,11 +252,7 @@ const checkBalances = async () => {
|
|
|
245
252
|
// Match service name (case-insensitive)
|
|
246
253
|
if (keyName.toLowerCase().includes(service.toLowerCase()) ||
|
|
247
254
|
service.toLowerCase().includes(keyName.toLowerCase())) {
|
|
248
|
-
balanceState.value[categoryName][keyName] =
|
|
249
|
-
loading: false,
|
|
250
|
-
error: false,
|
|
251
|
-
balance: balance
|
|
252
|
-
};
|
|
255
|
+
balanceState.value[categoryName][keyName] = normalizeBalanceState(balance);
|
|
253
256
|
}
|
|
254
257
|
});
|
|
255
258
|
});
|
|
@@ -285,7 +288,7 @@ const checkBalances = async () => {
|
|
|
285
288
|
};
|
|
286
289
|
|
|
287
290
|
const formatBalance = (balance) => {
|
|
288
|
-
if (typeof balance === 'number') {
|
|
291
|
+
if (typeof balance === 'number' && Number.isFinite(balance)) {
|
|
289
292
|
// Format large numbers with K, M suffixes if needed
|
|
290
293
|
if (balance >= 1000000) {
|
|
291
294
|
return (balance / 1000000).toFixed(1) + 'M';
|
|
@@ -296,7 +299,71 @@ const formatBalance = (balance) => {
|
|
|
296
299
|
}
|
|
297
300
|
return balance.toLocaleString();
|
|
298
301
|
}
|
|
299
|
-
return
|
|
302
|
+
return "--";
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const normalizeBalanceState = (rawBalance) => {
|
|
306
|
+
if (typeof rawBalance === "number" && Number.isFinite(rawBalance)) {
|
|
307
|
+
return {
|
|
308
|
+
loading: false,
|
|
309
|
+
error: false,
|
|
310
|
+
balance: rawBalance
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (typeof rawBalance === "string") {
|
|
315
|
+
const cleaned = rawBalance.trim();
|
|
316
|
+
const lower = cleaned.toLowerCase();
|
|
317
|
+
const isErrorLike =
|
|
318
|
+
lower.includes("could not check balance") ||
|
|
319
|
+
lower.includes("could not") ||
|
|
320
|
+
lower.includes("failed") ||
|
|
321
|
+
lower.includes("error") ||
|
|
322
|
+
lower.includes("invalid");
|
|
323
|
+
|
|
324
|
+
if (isErrorLike) {
|
|
325
|
+
return {
|
|
326
|
+
loading: false,
|
|
327
|
+
error: true,
|
|
328
|
+
balance: null
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const parsed = Number(cleaned.replace(/[$,\s]/g, ""));
|
|
333
|
+
if (Number.isFinite(parsed)) {
|
|
334
|
+
return {
|
|
335
|
+
loading: false,
|
|
336
|
+
error: false,
|
|
337
|
+
balance: parsed
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (rawBalance && typeof rawBalance === "object") {
|
|
343
|
+
const objectError = rawBalance.error || rawBalance.message || rawBalance.status;
|
|
344
|
+
if (objectError) {
|
|
345
|
+
return {
|
|
346
|
+
loading: false,
|
|
347
|
+
error: true,
|
|
348
|
+
balance: null
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const nestedBalance = rawBalance.balance ?? rawBalance.value;
|
|
353
|
+
if (typeof nestedBalance === "number" && Number.isFinite(nestedBalance)) {
|
|
354
|
+
return {
|
|
355
|
+
loading: false,
|
|
356
|
+
error: false,
|
|
357
|
+
balance: nestedBalance
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
loading: false,
|
|
364
|
+
error: true,
|
|
365
|
+
balance: null
|
|
366
|
+
};
|
|
300
367
|
};
|
|
301
368
|
|
|
302
369
|
loadProxyLists();
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
class="relative min-h-full grid-cols-10 gap-2 text-white lg:grid-cols-12"
|
|
4
4
|
@click="ui.setOpenContextMenu('')"
|
|
5
5
|
@dblclick="handleDoubleClick"
|
|
6
|
-
@touchstart="handleTouchStart"
|
|
7
|
-
@touchend="handleTouchEnd">
|
|
6
|
+
@touchstart.passive="handleTouchStart"
|
|
7
|
+
@touchend.passive="handleTouchEnd">
|
|
8
8
|
<div class="col-span-1 flex items-center justify-start py-2 lg:col-span-2">
|
|
9
9
|
<Checkbox
|
|
10
10
|
class="ml-2 mr-4 flex-shrink-0"
|
|
@@ -109,10 +109,9 @@
|
|
|
109
109
|
<span
|
|
110
110
|
class="mt-1 block text-xs font-bold"
|
|
111
111
|
:class="{
|
|
112
|
-
'text-red-400':
|
|
113
|
-
props.task._timeLeftString === '00:00' || props.task._timeLeftString === 'No Cartholds'
|
|
112
|
+
'text-red-400': taskTimeLeft === '00:00' || taskTimeLeft === 'No Cartholds'
|
|
114
113
|
}">
|
|
115
|
-
{{
|
|
114
|
+
{{ taskTimeLeft !== "00:00" ? taskTimeLeft : "Expired" }}
|
|
116
115
|
</span>
|
|
117
116
|
</h4>
|
|
118
117
|
</div>
|
|
@@ -144,7 +143,7 @@
|
|
|
144
143
|
<PlayIcon />
|
|
145
144
|
</button>
|
|
146
145
|
</li>
|
|
147
|
-
<li v-if="task.status?.toLowerCase() == 'waiting' &&
|
|
146
|
+
<li v-if="task.status?.toLowerCase() == 'waiting' && taskTimeLeft !== '00:00'">
|
|
148
147
|
<button @click="ui.continueTask(task.taskId, 'autocheckout')" aria-label="Auto checkout">
|
|
149
148
|
<BagWhiteIcon />
|
|
150
149
|
</button>
|
|
@@ -203,12 +202,13 @@ import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
|
|
|
203
202
|
import ActionButtonGroup from "@/components/ui/ActionButtonGroup.vue";
|
|
204
203
|
import EventDetailRow from "@/components/Tasks/EventDetailRow.vue";
|
|
205
204
|
import { useUIStore } from "@/stores/ui";
|
|
206
|
-
import { ref, onUnmounted, nextTick } from "vue";
|
|
205
|
+
import { computed, ref, onUnmounted, nextTick } from "vue";
|
|
207
206
|
import { useRowSelection } from "@/composables/useRowSelection";
|
|
208
207
|
import { useCopyToClipboard } from "@/composables/useCopyToClipboard";
|
|
209
208
|
import { useColorMapping } from "@/composables/useColorMapping";
|
|
210
209
|
import { useDateFormatting } from "@/composables/useDateFormatting";
|
|
211
210
|
import { useTicketPricing } from "@/composables/useTicketPricing";
|
|
211
|
+
import { timeDifference } from "@/libs/utils/time";
|
|
212
212
|
|
|
213
213
|
const ui = useUIStore();
|
|
214
214
|
const { copy } = useCopyToClipboard();
|
|
@@ -237,6 +237,19 @@ const { handleDoubleClick, handleTouchStart, handleTouchEnd } = useRowSelection(
|
|
|
237
237
|
ui.toggleTaskSelected(props.task.taskId);
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
const taskTimeLeft = computed(() => {
|
|
241
|
+
const now = ui.taskTimeTick || Date.now();
|
|
242
|
+
if (!props.task.expirationTime) return props.task._timeLeftString || "";
|
|
243
|
+
if (props.task.expirationTime === "Invalid Date") return "No Cartholds";
|
|
244
|
+
|
|
245
|
+
const asNumber = Number(props.task.expirationTime);
|
|
246
|
+
const expirationTimestamp = Number.isFinite(asNumber) && asNumber > 0
|
|
247
|
+
? asNumber
|
|
248
|
+
: Date.parse(props.task.expirationTime);
|
|
249
|
+
if (Number.isNaN(expirationTimestamp)) return props.task._timeLeftString || "00:00";
|
|
250
|
+
return timeDifference(expirationTimestamp, now);
|
|
251
|
+
});
|
|
252
|
+
|
|
240
253
|
const MENU_WIDTH = 168;
|
|
241
254
|
const MENU_HEIGHT = 200;
|
|
242
255
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="table-component relative box-border flex flex-col rounded-lg bg-dark-500 bg-clip-padding overflow-hidden shadow-sm">
|
|
2
|
+
<div class="table-component relative box-border flex flex-col rounded-lg bg-dark-500 bg-clip-padding overflow-x-hidden shadow-sm">
|
|
3
3
|
<Header class="grid-cols-10 gap-2 text-center lg:grid-cols-12">
|
|
4
4
|
<div class="col-span-1 flex items-center justify-start lg:col-span-2">
|
|
5
5
|
<Checkbox
|
|
@@ -34,28 +34,54 @@
|
|
|
34
34
|
<h4 class="text-center text-xs text-white">ID</h4>
|
|
35
35
|
</div>
|
|
36
36
|
</Header>
|
|
37
|
+
<DynamicScroller
|
|
38
|
+
v-if="virtualTaskItems.length"
|
|
39
|
+
class="hidden-scrollbars touch-pan-y min-h-0 overflow-y-auto overflow-x-hidden scrollable"
|
|
40
|
+
:style="{ height: dynamicTableHeight, maxHeight: dynamicTableHeight }"
|
|
41
|
+
:items="virtualTaskItems"
|
|
42
|
+
:min-item-size="virtualMinItemSize"
|
|
43
|
+
:buffer="virtualBuffer"
|
|
44
|
+
@wheel.passive="handleVirtualWheel"
|
|
45
|
+
key-field="taskId">
|
|
46
|
+
<template #default="{ item, index, active }">
|
|
47
|
+
<DynamicScrollerItem
|
|
48
|
+
:item="item"
|
|
49
|
+
:active="active"
|
|
50
|
+
:size-dependencies="[
|
|
51
|
+
props.tasks[item.taskId]?.status,
|
|
52
|
+
props.tasks[item.taskId]?.expirationTime,
|
|
53
|
+
ui.taskTimeTick,
|
|
54
|
+
props.tasks[item.taskId]?.reservedTicketsList,
|
|
55
|
+
props.tasks[item.taskId]?.eventName
|
|
56
|
+
]"
|
|
57
|
+
:data-index="index">
|
|
58
|
+
<div
|
|
59
|
+
class="shrink-0 border-b border-dark-650 min-h-14.5 md:min-h-17.25 has-[.event-details]:min-h-18.75 mobile-portrait:min-h-12.5 transition-colors duration-150 ease-in-out hover:!bg-dark-550">
|
|
60
|
+
<Task
|
|
61
|
+
v-if="props.tasks[item.taskId]"
|
|
62
|
+
:task="props.tasks[item.taskId]"
|
|
63
|
+
:preferEventName="props.preferEventName"
|
|
64
|
+
:class="getRowClass(index)" />
|
|
65
|
+
</div>
|
|
66
|
+
</DynamicScrollerItem>
|
|
67
|
+
</template>
|
|
68
|
+
</DynamicScroller>
|
|
37
69
|
<div
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<Task :task="task" :preferEventName="props.preferEventName" :class="getRowClass(i)" />
|
|
42
|
-
</div>
|
|
70
|
+
v-else
|
|
71
|
+
class="empty-state flex flex-col items-center justify-center py-8 text-center bg-dark-400 text-light-500 text-sm font-medium"
|
|
72
|
+
:style="{ minHeight: dynamicTableHeight, maxHeight: dynamicTableHeight }">
|
|
43
73
|
<div
|
|
44
|
-
v-if="
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
</
|
|
54
|
-
<
|
|
55
|
-
<TasksIcon class="mx-auto empty-state-icon" />
|
|
56
|
-
<p class="text-sm text-light-400">No tasks match current filters</p>
|
|
57
|
-
<p class="mt-1 text-xs text-light-500">Adjust filters to see tasks</p>
|
|
58
|
-
</div>
|
|
74
|
+
v-if="
|
|
75
|
+
!ui.queueStats.queued && !ui.queueStats.sleeping && ui.queueStats.nextQueuePasses.length === 0
|
|
76
|
+
">
|
|
77
|
+
<TasksIcon class="mx-auto empty-state-icon" />
|
|
78
|
+
<p class="text-sm text-light-400">No tasks yet</p>
|
|
79
|
+
<p class="mt-1 text-xs text-light-500">Create tasks to get started</p>
|
|
80
|
+
</div>
|
|
81
|
+
<div v-else>
|
|
82
|
+
<TasksIcon class="mx-auto empty-state-icon" />
|
|
83
|
+
<p class="text-sm text-light-400">No tasks match current filters</p>
|
|
84
|
+
<p class="mt-1 text-xs text-light-500">Adjust filters to see tasks</p>
|
|
59
85
|
</div>
|
|
60
86
|
</div>
|
|
61
87
|
</div>
|
|
@@ -68,6 +94,7 @@ import Task from "./Task.vue";
|
|
|
68
94
|
import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
|
|
69
95
|
import { useUIStore } from "@/stores/ui";
|
|
70
96
|
import { getRowClass } from "@/utils/tableHelpers";
|
|
97
|
+
import { DynamicScroller, DynamicScrollerItem } from "vue-virtual-scroller";
|
|
71
98
|
|
|
72
99
|
const props = defineProps({
|
|
73
100
|
tasks: {
|
|
@@ -99,47 +126,97 @@ const siteIdEdgeCases = {
|
|
|
99
126
|
TM_NZ: ["TM_AU"]
|
|
100
127
|
};
|
|
101
128
|
|
|
102
|
-
const
|
|
103
|
-
|
|
129
|
+
const virtualItemCache = new Map();
|
|
130
|
+
const searchTextCache = new Map();
|
|
131
|
+
|
|
132
|
+
const shouldTaskMatchSearch = (task, searchLower) => {
|
|
133
|
+
if (!searchLower) return true;
|
|
134
|
+
|
|
135
|
+
const cached = searchTextCache.get(task.taskId);
|
|
136
|
+
if (
|
|
137
|
+
cached &&
|
|
138
|
+
cached.eventId === task.eventId &&
|
|
139
|
+
cached.eventName === task.eventName &&
|
|
140
|
+
cached.eventVenue === task.eventVenue &&
|
|
141
|
+
cached.email === task.email &&
|
|
142
|
+
cached.status === task.status &&
|
|
143
|
+
cached.profileName === task.profileName &&
|
|
144
|
+
cached.presaleCode === task.presaleCode &&
|
|
145
|
+
cached.reservedTicketsList === task.reservedTicketsList
|
|
146
|
+
) {
|
|
147
|
+
return cached.text.includes(searchLower);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const searchableText = [
|
|
151
|
+
task.eventId,
|
|
152
|
+
task.eventName,
|
|
153
|
+
task.eventVenue,
|
|
154
|
+
task.email,
|
|
155
|
+
task.taskId,
|
|
156
|
+
task.status,
|
|
157
|
+
task.profileName,
|
|
158
|
+
task.presaleCode,
|
|
159
|
+
task.reservedTicketsList
|
|
160
|
+
]
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.join(" ")
|
|
163
|
+
.toLowerCase();
|
|
164
|
+
|
|
165
|
+
searchTextCache.set(task.taskId, {
|
|
166
|
+
eventId: task.eventId,
|
|
167
|
+
eventName: task.eventName,
|
|
168
|
+
eventVenue: task.eventVenue,
|
|
169
|
+
email: task.email,
|
|
170
|
+
status: task.status,
|
|
171
|
+
profileName: task.profileName,
|
|
172
|
+
presaleCode: task.presaleCode,
|
|
173
|
+
reservedTicketsList: task.reservedTicketsList,
|
|
174
|
+
text: searchableText
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return searchableText.includes(searchLower);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const filteredTaskIds = computed(() => {
|
|
181
|
+
const out = [];
|
|
104
182
|
const searchLower = props.searchQuery?.toLowerCase().trim() || "";
|
|
105
183
|
|
|
106
|
-
ui.taskIdOrder
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
task.siteId !== ui.currentCountry.siteId &&
|
|
112
|
-
!siteIdEdgeCases[task.siteId]?.includes(ui.currentCountry.siteId)
|
|
113
|
-
)
|
|
114
|
-
return;
|
|
115
|
-
if (ui.currentEvent && task.eventId !== ui.currentEvent) return;
|
|
116
|
-
if (!shouldTaskShow(task)) return;
|
|
117
|
-
|
|
118
|
-
// Search filter
|
|
119
|
-
if (searchLower) {
|
|
120
|
-
const searchableText = [
|
|
121
|
-
task.eventId,
|
|
122
|
-
task.eventName,
|
|
123
|
-
task.eventVenue,
|
|
124
|
-
task.email,
|
|
125
|
-
task.taskId,
|
|
126
|
-
task.status,
|
|
127
|
-
task.profileName,
|
|
128
|
-
task.presaleCode,
|
|
129
|
-
task.reservedTicketsList
|
|
130
|
-
]
|
|
131
|
-
.filter(Boolean)
|
|
132
|
-
.join(" ")
|
|
133
|
-
.toLowerCase();
|
|
134
|
-
|
|
135
|
-
if (!searchableText.includes(searchLower)) return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
out.push(task);
|
|
184
|
+
for (const id of ui.taskIdOrder) {
|
|
185
|
+
const task = props.tasks[id];
|
|
186
|
+
if (!task || task.hidden) {
|
|
187
|
+
searchTextCache.delete(id);
|
|
188
|
+
continue;
|
|
139
189
|
}
|
|
140
|
-
|
|
190
|
+
|
|
191
|
+
if (task.siteId !== ui.currentCountry.siteId && !siteIdEdgeCases[task.siteId]?.includes(ui.currentCountry.siteId)) continue;
|
|
192
|
+
if (ui.currentEvent && task.eventId !== ui.currentEvent) continue;
|
|
193
|
+
if (!shouldTaskShow(task)) continue;
|
|
194
|
+
if (!shouldTaskMatchSearch(task, searchLower)) continue;
|
|
195
|
+
|
|
196
|
+
out.push(id);
|
|
197
|
+
}
|
|
198
|
+
|
|
141
199
|
return out;
|
|
142
|
-
};
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const virtualTaskItems = computed(() => {
|
|
203
|
+
const idsInUse = new Set(filteredTaskIds.value);
|
|
204
|
+
for (const cachedId of virtualItemCache.keys()) {
|
|
205
|
+
if (!idsInUse.has(cachedId)) virtualItemCache.delete(cachedId);
|
|
206
|
+
}
|
|
207
|
+
for (const cachedId of searchTextCache.keys()) {
|
|
208
|
+
if (!idsInUse.has(cachedId) && !props.tasks[cachedId]) searchTextCache.delete(cachedId);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return filteredTaskIds.value.map((taskId) => {
|
|
212
|
+
let item = virtualItemCache.get(taskId);
|
|
213
|
+
if (!item) {
|
|
214
|
+
item = { taskId };
|
|
215
|
+
virtualItemCache.set(taskId, item);
|
|
216
|
+
}
|
|
217
|
+
return item;
|
|
218
|
+
});
|
|
219
|
+
});
|
|
143
220
|
|
|
144
221
|
// Dynamic height calculation to prevent page scrolling
|
|
145
222
|
const windowHeight = ref(window.innerHeight);
|
|
@@ -158,6 +235,15 @@ onUnmounted(() => {
|
|
|
158
235
|
window.removeEventListener("resize", updateDimensions);
|
|
159
236
|
});
|
|
160
237
|
|
|
238
|
+
const virtualMinItemSize = computed(() => (windowWidth.value <= 768 ? 58 : 69));
|
|
239
|
+
const virtualBuffer = computed(() => virtualMinItemSize.value * 8);
|
|
240
|
+
|
|
241
|
+
const handleVirtualWheel = (event) => {
|
|
242
|
+
const target = event.currentTarget;
|
|
243
|
+
if (!target) return;
|
|
244
|
+
target.scrollTop += event.deltaY;
|
|
245
|
+
};
|
|
246
|
+
|
|
161
247
|
const dynamicTableHeight = computed(() => {
|
|
162
248
|
// Detect PWA mode (standalone display)
|
|
163
249
|
const isPWA = window.matchMedia('(display-mode: standalone)').matches;
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
<span>{{ taskSnapshot.status }}</span>
|
|
48
48
|
</InfoRow>
|
|
49
49
|
<InfoRow
|
|
50
|
-
v-if="
|
|
50
|
+
v-if="taskTimeLeft && taskTimeLeft !== 'No Cartholds'"
|
|
51
51
|
:icon="TimerIcon"
|
|
52
52
|
label="Cart Expiration"
|
|
53
|
-
:value-class="`font-semibold text-right ${
|
|
54
|
-
{{
|
|
53
|
+
:value-class="`font-semibold text-right ${taskTimeLeft === '00:00' ? 'text-red-400' : ''}`">
|
|
54
|
+
{{ taskTimeLeft !== "00:00" ? taskTimeLeft : "Expired" }}
|
|
55
55
|
</InfoRow>
|
|
56
56
|
<InfoRow
|
|
57
57
|
v-if="taskSnapshot.reservedTicketsList"
|
|
@@ -192,6 +192,7 @@ import { useCopyToClipboard } from "@/composables/useCopyToClipboard";
|
|
|
192
192
|
import { useColorMapping } from "@/composables/useColorMapping";
|
|
193
193
|
import { useDateFormatting } from "@/composables/useDateFormatting";
|
|
194
194
|
import { useTicketPricing } from "@/composables/useTicketPricing";
|
|
195
|
+
import { timeDifference } from "@/libs/utils/time";
|
|
195
196
|
import {
|
|
196
197
|
EyeIcon,
|
|
197
198
|
StadiumIcon,
|
|
@@ -260,4 +261,17 @@ const toggles = computed(() => ({
|
|
|
260
261
|
quickQueue: taskSnapshot.value.quickQueue,
|
|
261
262
|
agedAccount: taskSnapshot.value.agedAccount
|
|
262
263
|
}));
|
|
264
|
+
|
|
265
|
+
const taskTimeLeft = computed(() => {
|
|
266
|
+
const now = ui.taskTimeTick || Date.now();
|
|
267
|
+
if (!taskSnapshot.value.expirationTime) return taskSnapshot.value._timeLeftString || "";
|
|
268
|
+
if (taskSnapshot.value.expirationTime === "Invalid Date") return "No Cartholds";
|
|
269
|
+
|
|
270
|
+
const asNumber = Number(taskSnapshot.value.expirationTime);
|
|
271
|
+
const expirationTimestamp = Number.isFinite(asNumber) && asNumber > 0
|
|
272
|
+
? asNumber
|
|
273
|
+
: Date.parse(taskSnapshot.value.expirationTime);
|
|
274
|
+
if (Number.isNaN(expirationTimestamp)) return taskSnapshot.value._timeLeftString || "00:00";
|
|
275
|
+
return timeDifference(expirationTimestamp, now);
|
|
276
|
+
});
|
|
263
277
|
</script>
|