@7365admin1/layer-common 1.10.5 → 1.10.7
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/CHANGELOG.md +12 -0
- package/components/AccessCardDetailsDialog.vue +144 -0
- package/components/AccessCardPreviewDialog.vue +7 -2
- package/components/AccessCardQrTagging.vue +314 -34
- package/components/AccessCardQrTaggingPrintQr.vue +75 -0
- package/components/AccessManagement.vue +5 -1
- package/components/AreaChecklistHistoryLogs.vue +9 -0
- package/components/BuildingForm.vue +36 -5
- package/components/BuildingManagement/buildings.vue +18 -9
- package/components/BuildingManagement/units.vue +12 -114
- package/components/BuildingUnitFormAdd.vue +39 -30
- package/components/BuildingUnitFormEdit.vue +265 -116
- package/components/CleaningScheduleMain.vue +60 -13
- package/components/Dialog/DeleteConfirmation.vue +2 -2
- package/components/Dialog/UpdateMoreAction.vue +2 -2
- package/components/EntryPassInformation.vue +215 -0
- package/components/Input/InputPhoneNumberV2.vue +11 -0
- package/components/ManageChecklistMain.vue +29 -3
- package/components/PlateNumberDisplay.vue +9 -1
- package/components/ScheduleAreaMain.vue +56 -0
- package/components/TableHygiene.vue +265 -113
- package/components/UnitPersonCard.vue +63 -0
- package/components/VehicleAddSelection.vue +2 -2
- package/components/VehicleForm.vue +169 -47
- package/components/VehicleManagement.vue +169 -43
- package/components/VisitorForm.vue +19 -0
- package/components/VisitorManagement.vue +1 -1
- package/components/WorkOrder/Main.vue +2 -1
- package/composables/useAccessManagement.ts +63 -0
- package/composables/useFeedback.ts +2 -2
- package/composables/usePeople.ts +14 -3
- package/composables/useVehicle.ts +15 -1
- package/composables/useWorkOrder.ts +2 -2
- package/package.json +3 -2
- package/types/cleaner-schedule.d.ts +1 -0
- package/types/html2pdf.d.ts +19 -0
- package/types/people.d.ts +4 -0
- package/types/site.d.ts +8 -0
- package/types/vehicle.d.ts +3 -4
- package/.playground/app.vue +0 -41
- package/.playground/eslint.config.mjs +0 -6
- package/.playground/nuxt.config.ts +0 -22
- package/.playground/pages/feedback.vue +0 -30
- package/components/AccessCardHistoryDialog.vue +0 -133
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<v-skeleton-loader v-if="loading" type="list-item-two-line" width="100%" />
|
|
4
|
+
<template v-else>
|
|
5
|
+
<v-divider class="mb-4" />
|
|
6
|
+
<InputLabel title="Pass Type (Optional)" />
|
|
7
|
+
<v-row no-gutters class="ga-3 mt-1">
|
|
8
|
+
<v-col>
|
|
9
|
+
<v-card
|
|
10
|
+
:variant="passType === 'QR' ? 'tonal' : 'outlined'"
|
|
11
|
+
:color="passType === 'QR' ? 'primary' : undefined"
|
|
12
|
+
class="pa-3 d-flex flex-column align-center ga-2 cursor-pointer position-relative"
|
|
13
|
+
rounded="lg"
|
|
14
|
+
@click="passType = passType === 'QR' ? null : 'QR'"
|
|
15
|
+
>
|
|
16
|
+
<v-icon v-if="passType === 'QR'" size="18" color="primary" class="selected-check">mdi-check-circle</v-icon>
|
|
17
|
+
<v-icon size="32">mdi-qrcode</v-icon>
|
|
18
|
+
<span class="text-subtitle-2 font-weight-bold">QR Pass</span>
|
|
19
|
+
</v-card>
|
|
20
|
+
</v-col>
|
|
21
|
+
<v-col v-if="nfcEnabled">
|
|
22
|
+
<v-card
|
|
23
|
+
:variant="passType === 'NFC' ? 'tonal' : 'outlined'"
|
|
24
|
+
:color="passType === 'NFC' ? 'primary' : undefined"
|
|
25
|
+
class="pa-3 d-flex flex-column align-center ga-2 cursor-pointer position-relative"
|
|
26
|
+
rounded="lg"
|
|
27
|
+
@click="passType = passType === 'NFC' ? null : 'NFC'"
|
|
28
|
+
>
|
|
29
|
+
<v-icon v-if="passType === 'NFC'" size="18" color="primary" class="selected-check">mdi-check-circle</v-icon>
|
|
30
|
+
<v-icon size="32">mdi-nfc</v-icon>
|
|
31
|
+
<span class="text-subtitle-2 font-weight-bold">NFC</span>
|
|
32
|
+
</v-card>
|
|
33
|
+
</v-col>
|
|
34
|
+
</v-row>
|
|
35
|
+
|
|
36
|
+
<!-- QR Pass section -->
|
|
37
|
+
<template v-if="passType === 'QR'">
|
|
38
|
+
<v-alert
|
|
39
|
+
v-if="!hasPrinter"
|
|
40
|
+
type="warning"
|
|
41
|
+
variant="tonal"
|
|
42
|
+
density="compact"
|
|
43
|
+
class="mt-3"
|
|
44
|
+
icon="mdi-printer-alert"
|
|
45
|
+
>
|
|
46
|
+
No printer is configured in the settings. QR Pass may not print.
|
|
47
|
+
</v-alert>
|
|
48
|
+
<template v-else>
|
|
49
|
+
<InputLabel title="Quantity" class="mt-3" />
|
|
50
|
+
<v-text-field
|
|
51
|
+
v-model.number="quantity"
|
|
52
|
+
type="number"
|
|
53
|
+
density="comfortable"
|
|
54
|
+
:min="1"
|
|
55
|
+
hide-details
|
|
56
|
+
/>
|
|
57
|
+
</template>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<!-- NFC Pass section -->
|
|
61
|
+
<template v-if="passType === 'NFC'">
|
|
62
|
+
<div class="mt-3">
|
|
63
|
+
<InputLabel title="Select Cards" />
|
|
64
|
+
<v-select
|
|
65
|
+
v-model="selectedCards"
|
|
66
|
+
:items="cards"
|
|
67
|
+
:loading="cardsLoading"
|
|
68
|
+
item-title="name"
|
|
69
|
+
item-value="_id"
|
|
70
|
+
density="comfortable"
|
|
71
|
+
multiple
|
|
72
|
+
chips
|
|
73
|
+
closable-chips
|
|
74
|
+
hide-details
|
|
75
|
+
placeholder="Select cards..."
|
|
76
|
+
>
|
|
77
|
+
<template #item="{ props: itemProps, item }">
|
|
78
|
+
<v-list-item v-bind="itemProps">
|
|
79
|
+
<template #subtitle>
|
|
80
|
+
<span class="text-caption text-grey">{{ item.raw.cardNumber }}</span>
|
|
81
|
+
</template>
|
|
82
|
+
</v-list-item>
|
|
83
|
+
</template>
|
|
84
|
+
</v-select>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="mt-3">
|
|
88
|
+
<InputLabel title="Quantity" />
|
|
89
|
+
<v-text-field
|
|
90
|
+
:model-value="selectedCards.length"
|
|
91
|
+
type="number"
|
|
92
|
+
density="comfortable"
|
|
93
|
+
disabled
|
|
94
|
+
hide-details
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<v-row no-gutters class="ga-3 mt-3">
|
|
99
|
+
<v-col>
|
|
100
|
+
<v-btn
|
|
101
|
+
variant="outlined"
|
|
102
|
+
color="primary"
|
|
103
|
+
block
|
|
104
|
+
prepend-icon="mdi-barcode-scan"
|
|
105
|
+
@click="emit('scan:barcode')"
|
|
106
|
+
>
|
|
107
|
+
Scan Via BarCode
|
|
108
|
+
</v-btn>
|
|
109
|
+
</v-col>
|
|
110
|
+
<v-col>
|
|
111
|
+
<v-btn
|
|
112
|
+
variant="outlined"
|
|
113
|
+
color="primary"
|
|
114
|
+
block
|
|
115
|
+
prepend-icon="mdi-camera"
|
|
116
|
+
@click="emit('scan:camera')"
|
|
117
|
+
>
|
|
118
|
+
Scan with Camera
|
|
119
|
+
</v-btn>
|
|
120
|
+
</v-col>
|
|
121
|
+
</v-row>
|
|
122
|
+
</template>
|
|
123
|
+
</template>
|
|
124
|
+
<v-divider class="mt-6" />
|
|
125
|
+
</div>
|
|
126
|
+
</template>
|
|
127
|
+
|
|
128
|
+
<script setup lang="ts">
|
|
129
|
+
const props = defineProps({
|
|
130
|
+
settings: {
|
|
131
|
+
type: Object as PropType<Record<string, any> | null>,
|
|
132
|
+
default: null,
|
|
133
|
+
},
|
|
134
|
+
loading: {
|
|
135
|
+
type: Boolean,
|
|
136
|
+
default: false,
|
|
137
|
+
},
|
|
138
|
+
modelValue: {
|
|
139
|
+
type: String as PropType<string | null>,
|
|
140
|
+
default: null,
|
|
141
|
+
},
|
|
142
|
+
quantity: {
|
|
143
|
+
type: Number as PropType<number | null>,
|
|
144
|
+
default: null,
|
|
145
|
+
},
|
|
146
|
+
cards: {
|
|
147
|
+
type: Array as PropType<string[]>,
|
|
148
|
+
default: () => [],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const emit = defineEmits(["update:modelValue", "update:quantity", "update:cards", "scan:barcode", "scan:camera"]);
|
|
153
|
+
|
|
154
|
+
const { getAll: getAllCards } = useCard();
|
|
155
|
+
|
|
156
|
+
const nfcEnabled = computed(
|
|
157
|
+
() => props.settings?.data?.settings?.nfcPass ?? false
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const printer = computed(
|
|
161
|
+
() => props.settings?.data?.settings?.printer ?? { vendorId: null, productId: null }
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const hasPrinter = computed(
|
|
165
|
+
() => !!printer.value.vendorId && !!printer.value.productId
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const passType = computed({
|
|
169
|
+
get: () => props.modelValue,
|
|
170
|
+
set: (val) => emit("update:modelValue", val),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const quantity = computed({
|
|
174
|
+
get: () => props.quantity,
|
|
175
|
+
set: (val) => emit("update:quantity", val),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const selectedCards = computed({
|
|
179
|
+
get: () => props.cards,
|
|
180
|
+
set: (val) => {
|
|
181
|
+
emit("update:cards", val);
|
|
182
|
+
emit("update:quantity", val.length || null);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const cardItems = ref<TCard[]>([]);
|
|
187
|
+
const cardsLoading = ref(false);
|
|
188
|
+
|
|
189
|
+
const cards = computed(() => cardItems.value);
|
|
190
|
+
|
|
191
|
+
async function fetchCards() {
|
|
192
|
+
cardsLoading.value = true;
|
|
193
|
+
try {
|
|
194
|
+
const res = await getAllCards({ assignFilter: "available", limit: 100 });
|
|
195
|
+
cardItems.value = res?.data ?? [];
|
|
196
|
+
} finally {
|
|
197
|
+
cardsLoading.value = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
watch(
|
|
202
|
+
() => passType.value,
|
|
203
|
+
(val) => {
|
|
204
|
+
if (val === "NFC") fetchCards();
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
</script>
|
|
208
|
+
|
|
209
|
+
<style scoped>
|
|
210
|
+
.selected-check {
|
|
211
|
+
position: absolute;
|
|
212
|
+
top: 6px;
|
|
213
|
+
right: 6px;
|
|
214
|
+
}
|
|
215
|
+
</style>
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
style="max-width: 95px"
|
|
14
14
|
:rules="[...props.rules]"
|
|
15
15
|
:readonly="props.readOnly"
|
|
16
|
+
:disabled="props.disabled"
|
|
16
17
|
@update:model-value="handleUpdateCountry"
|
|
17
18
|
>
|
|
18
19
|
<template v-slot:item="{ props: itemProps, item }">
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
:prefix="phonePrefix || ''"
|
|
41
42
|
persistent-placeholder
|
|
42
43
|
:density="density"
|
|
44
|
+
:disabled="props.disabled"
|
|
43
45
|
:placeholder="placeholder || currentMask"
|
|
44
46
|
/>
|
|
45
47
|
</v-col>
|
|
@@ -64,6 +66,7 @@ const props = defineProps({
|
|
|
64
66
|
hideDetails: { type: Boolean, default: false },
|
|
65
67
|
loading: { type: Boolean, default: false },
|
|
66
68
|
readOnly: { type: Boolean, default: false },
|
|
69
|
+
disabled: { type: Boolean, default: false },
|
|
67
70
|
})
|
|
68
71
|
|
|
69
72
|
const emit = defineEmits(['update:modelValue'])
|
|
@@ -138,6 +141,14 @@ watch(input, (newInput) => {
|
|
|
138
141
|
else phone.value = prefix + newInput
|
|
139
142
|
})
|
|
140
143
|
|
|
144
|
+
watch(
|
|
145
|
+
() => props.modelValue,
|
|
146
|
+
(val) => {
|
|
147
|
+
phone.value = val || ''
|
|
148
|
+
},
|
|
149
|
+
{ immediate: true }
|
|
150
|
+
)
|
|
151
|
+
|
|
141
152
|
|
|
142
153
|
watch(phone, (newVal) => {
|
|
143
154
|
const prefix = phonePrefix.value
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
Back
|
|
9
9
|
</v-btn>
|
|
10
10
|
</v-col>
|
|
11
|
+
<v-spacer />
|
|
12
|
+
<v-col v-if="isScheduleClosed" cols="auto" class="d-flex align-center">
|
|
13
|
+
<v-chip color="error" variant="tonal" class="text-capitalize">
|
|
14
|
+
<v-icon start>mdi-lock</v-icon>
|
|
15
|
+
Closed – View Only
|
|
16
|
+
</v-chip>
|
|
17
|
+
</v-col>
|
|
11
18
|
</v-row>
|
|
12
19
|
<TableHygiene
|
|
13
20
|
ref="tableHygieneRef"
|
|
@@ -22,8 +29,8 @@
|
|
|
22
29
|
:loading="loading"
|
|
23
30
|
:no-data-text="`No checklist found`"
|
|
24
31
|
:show-header="true"
|
|
25
|
-
:can-manage-schedule-tasks="canManageScheduleTasks"
|
|
26
|
-
:can-add-remarks="canAddRemarks"
|
|
32
|
+
:can-manage-schedule-tasks="!isScheduleClosed && canManageScheduleTasks"
|
|
33
|
+
:can-add-remarks="!isScheduleClosed && canAddRemarks"
|
|
27
34
|
@refresh="getUnitCleanerChecklistRefresh"
|
|
28
35
|
@update:selected="selectedItems = $event"
|
|
29
36
|
@action-click="handleActionClick"
|
|
@@ -113,6 +120,9 @@ const { back } = useUtils();
|
|
|
113
120
|
const { canAddRemarks, canManageScheduleTasks } =
|
|
114
121
|
useCleaningSchedulePermission();
|
|
115
122
|
|
|
123
|
+
const selectedScheduleStatus = useState<string>('selectedScheduleStatus', () => '')
|
|
124
|
+
const isScheduleClosed = computed(() => selectedScheduleStatus.value.toLowerCase() === 'closed')
|
|
125
|
+
|
|
116
126
|
const page = ref<number>(1);
|
|
117
127
|
const pages = ref<number>(0);
|
|
118
128
|
const pageRange = ref<string>("-- - -- of --");
|
|
@@ -241,6 +251,11 @@ function openCompletionDialog({
|
|
|
241
251
|
approvedItems: Array<{ key: string; item: any; action: "approve" }>;
|
|
242
252
|
lastApprovedKey: string | null;
|
|
243
253
|
}): void {
|
|
254
|
+
if (isScheduleClosed.value) {
|
|
255
|
+
showMessage("This schedule is closed. No actions can be performed.", "error");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
244
259
|
// If user does not need to add remarks, perform approvals immediately
|
|
245
260
|
if (!canAddRemarks.value) {
|
|
246
261
|
// call update directly without opening modal
|
|
@@ -283,6 +298,11 @@ async function handleActionClick(data: {
|
|
|
283
298
|
item: TFlattenedUnitItem;
|
|
284
299
|
action: "approve" | "reject";
|
|
285
300
|
}): Promise<void> {
|
|
301
|
+
if (isScheduleClosed.value) {
|
|
302
|
+
showMessage("This schedule is closed. No actions can be performed.", "error");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
286
306
|
const { item, action } = data;
|
|
287
307
|
|
|
288
308
|
if (!item?.unit || item?.set === undefined) {
|
|
@@ -322,9 +342,15 @@ async function handleActionClick(data: {
|
|
|
322
342
|
await getUnitCleanerChecklistRefresh();
|
|
323
343
|
} catch (error: any) {
|
|
324
344
|
console.error("Error updating unit checklist:", error);
|
|
325
|
-
showMessage(
|
|
345
|
+
showMessage(
|
|
346
|
+
error?.data?.message || error?.message || "Failed to update checklist",
|
|
347
|
+
"error"
|
|
348
|
+
);
|
|
326
349
|
} finally {
|
|
327
350
|
submitting.value = false;
|
|
351
|
+
if (action === "approve") {
|
|
352
|
+
tableHygieneRef.value?.stopLoadingAction(`${item.unit}_${item.set}`);
|
|
353
|
+
}
|
|
328
354
|
}
|
|
329
355
|
}
|
|
330
356
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</span>
|
|
6
6
|
|
|
7
7
|
<v-chip
|
|
8
|
-
v-if="extraCount > 0"
|
|
8
|
+
v-if="extraCount > 0 && !showAll"
|
|
9
9
|
density="comfortable"
|
|
10
10
|
size="small"
|
|
11
11
|
>
|
|
@@ -23,12 +23,20 @@ const props = defineProps({
|
|
|
23
23
|
defaultValue: {
|
|
24
24
|
type: String,
|
|
25
25
|
default: ""
|
|
26
|
+
},
|
|
27
|
+
showAll: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
default: false
|
|
26
30
|
}
|
|
27
31
|
})
|
|
28
32
|
|
|
29
33
|
const formatted = computed(() => {
|
|
30
34
|
if (!props.plateNumbers?.length) return props.defaultValue || ""
|
|
31
35
|
|
|
36
|
+
if (props.showAll) {
|
|
37
|
+
return props.plateNumbers.map((v: any) => v?.plateNumber || "").join(", ")
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
const firstTwo = props.plateNumbers
|
|
33
41
|
.slice(0, 2)
|
|
34
42
|
.map((v: any) => v?.plateNumber || "")
|
|
@@ -68,6 +68,15 @@
|
|
|
68
68
|
<v-spacer />
|
|
69
69
|
|
|
70
70
|
<v-col cols="auto">
|
|
71
|
+
<v-chip
|
|
72
|
+
v-if="isScheduleClosed"
|
|
73
|
+
color="error"
|
|
74
|
+
variant="tonal"
|
|
75
|
+
class="text-capitalize mr-2"
|
|
76
|
+
>
|
|
77
|
+
<v-icon start>mdi-lock</v-icon>
|
|
78
|
+
Closed – View Only
|
|
79
|
+
</v-chip>
|
|
71
80
|
<v-btn
|
|
72
81
|
v-if="canViewHistory"
|
|
73
82
|
class="text-none mr-2"
|
|
@@ -86,6 +95,7 @@
|
|
|
86
95
|
aria-label="Generate"
|
|
87
96
|
@click="_generateChecklist"
|
|
88
97
|
:loading="submitting"
|
|
98
|
+
:disabled="isScheduleClosed"
|
|
89
99
|
>
|
|
90
100
|
Generate List
|
|
91
101
|
</v-btn>
|
|
@@ -103,6 +113,15 @@
|
|
|
103
113
|
{{ item.acceptedByName || item.acceptedBy || "Accepted" }}
|
|
104
114
|
</span>
|
|
105
115
|
</div>
|
|
116
|
+
<div v-else-if="isScheduleClosed">
|
|
117
|
+
<v-chip
|
|
118
|
+
size="small"
|
|
119
|
+
color="error"
|
|
120
|
+
variant="tonal"
|
|
121
|
+
class="text-capitalize"
|
|
122
|
+
>Closed</v-chip
|
|
123
|
+
>
|
|
124
|
+
</div>
|
|
106
125
|
<div v-else>
|
|
107
126
|
<v-btn
|
|
108
127
|
v-if="canManageScheduleTasks"
|
|
@@ -176,6 +195,14 @@ const { formatDate, back } = useUtils();
|
|
|
176
195
|
const { canGenerateChecklist, canViewHistory, canManageScheduleTasks } =
|
|
177
196
|
useCleaningSchedulePermission();
|
|
178
197
|
|
|
198
|
+
const selectedScheduleStatus = useState<string>(
|
|
199
|
+
"selectedScheduleStatus",
|
|
200
|
+
() => ""
|
|
201
|
+
);
|
|
202
|
+
const isScheduleClosed = computed(
|
|
203
|
+
() => selectedScheduleStatus.value.toLowerCase() === "closed"
|
|
204
|
+
);
|
|
205
|
+
|
|
179
206
|
const items = ref<Array<Record<string, any>>>([]);
|
|
180
207
|
|
|
181
208
|
const statusFilter = ref<TScheduleAreaStatus>("All");
|
|
@@ -313,4 +340,33 @@ async function _generateChecklist() {
|
|
|
313
340
|
submitting.value = false;
|
|
314
341
|
}
|
|
315
342
|
}
|
|
343
|
+
|
|
344
|
+
onMounted(() => {
|
|
345
|
+
try {
|
|
346
|
+
const { $socket } = useNuxtApp() as any;
|
|
347
|
+
if ($socket) {
|
|
348
|
+
$socket.emit("join:cleaning-area", props.scheduleAreaId);
|
|
349
|
+
|
|
350
|
+
$socket.on(
|
|
351
|
+
"area-checklist:generated",
|
|
352
|
+
(payload: { scheduleId: string; siteId: string }) => {
|
|
353
|
+
if (payload?.scheduleId !== props.scheduleAreaId) return;
|
|
354
|
+
if (typeof getScheduleAreasRefresh === "function") {
|
|
355
|
+
getScheduleAreasRefresh();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
} catch (_) {}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
onUnmounted(() => {
|
|
364
|
+
try {
|
|
365
|
+
const { $socket } = useNuxtApp() as any;
|
|
366
|
+
if ($socket) {
|
|
367
|
+
$socket.emit("leave:cleaning-area", props.scheduleAreaId);
|
|
368
|
+
$socket.off("area-checklist:generated");
|
|
369
|
+
}
|
|
370
|
+
} catch (_) {}
|
|
371
|
+
});
|
|
316
372
|
</script>
|