@7365admin1/layer-common 4.96.0 → 4.97.1-staging.500
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 +10 -4
- package/assets/css/primitives.css +57 -0
- package/assets/css/screens.css +8 -4
- package/components/AccessCardDetailsDialog.vue +59 -6
- package/components/AccessCardPreviewDialog.vue +371 -53
- package/components/AppSelect.vue +143 -4
- package/components/BuildingUnitFormEdit.vue +6 -0
- package/components/BuildingUnitManagement.vue +298 -0
- package/components/Dialog/UpdateMoreAction.vue +11 -1
- package/components/DocumentForm.vue +407 -112
- package/components/DocumentManagement.vue +213 -12
- package/components/Facility/BookingSetup.vue +63 -0
- package/components/HidAccessLogDashboard.vue +74 -17
- package/components/HidAccessPermissions.vue +424 -0
- package/components/HidIntercomManagement.vue +183 -14
- package/components/HidProfileQrCode.vue +332 -0
- package/components/HidQrCodeConfiguration.vue +17 -195
- package/components/HidReaderManagement.vue +398 -0
- package/components/HidReaderUserRoster.vue +44 -3
- package/components/HidUserEnrollment.vue +2239 -276
- package/components/InvitationClientForm.vue +19 -1
- package/components/Nfc/NFCPatrolRouteForm.vue +109 -58
- package/components/Nfc/NFCPatrolRouteMain.vue +3 -18
- package/components/Nfc/PatrolReport/PatrolActivityTable.vue +0 -8
- package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +1 -1
- package/components/ServiceProviderMain.vue +48 -0
- package/components/SiteProviderChange.vue +371 -0
- package/components/TableMain.vue +22 -7
- package/components/VehicleQrStickerDialog.vue +64 -4
- package/components/VisitorForm.vue +12 -0
- package/components/VisitorManagement.vue +267 -5
- package/composables/useAccessManagement.ts +17 -1
- package/composables/useCustomerSite.ts +29 -0
- package/composables/useDocument.ts +7 -0
- package/composables/useFacility.ts +11 -0
- package/composables/useHidAmico.ts +165 -0
- package/composables/useHidNavigation.ts +25 -1
- package/composables/useHidReaderSelection.ts +50 -0
- package/composables/useMember.ts +12 -0
- package/composables/useNFCPatrolReport.ts +36 -32
- package/composables/useNFCPatrolReportFilters.ts +21 -3
- package/composables/useNFCPatrolRoute.ts +10 -3
- package/composables/usePeople.ts +19 -0
- package/composables/useServiceProvider.ts +45 -2
- package/composables/useSiteCategory.ts +45 -0
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +23 -0
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
- package/types/document.d.ts +14 -0
- package/types/facility.d.ts +4 -0
- package/types/member.d.ts +4 -0
- package/types/nfc-patrol-report.ts +0 -2
- package/types/people.d.ts +28 -1
- package/types/service-provider.d.ts +5 -0
- package/types/site.d.ts +7 -1
- package/utils/hid-enrolment-subject.ts +344 -0
- package/utils/hid-permission-assignments.ts +198 -0
- package/utils/hid-reader-selection.ts +52 -0
- package/utils/nfc-patrol-time.ts +99 -0
- package/utils/occupancy-role.ts +109 -0
- package/utils/service-type.ts +30 -0
- package/utils/vehicle-qr-design.ts +304 -20
- package/utils/vehicle-qr-sticker.ts +126 -20
|
@@ -388,9 +388,27 @@ watch(
|
|
|
388
388
|
const roles = ref<any[]>([])
|
|
389
389
|
const { getRoles } = useRole()
|
|
390
390
|
|
|
391
|
+
/*
|
|
392
|
+
* `type` is the ROLE type, not the service the client subscribes to, and the
|
|
393
|
+
* only caller (`ClientMain.vue` "Invite Client") passes no `app` prop -- so
|
|
394
|
+
* this sent `type: ""` and the endpoint applied no type filter at all. The
|
|
395
|
+
* dropdown therefore listed every org-less role of every type, which includes
|
|
396
|
+
* the platform's OWN staff roles (`type: "admin"` -- the two rows the console's
|
|
397
|
+
* `/super-admin/role-permissions` screen edits). Offering a Seven365 console
|
|
398
|
+
* staff role on a CLIENT invitation is the defect.
|
|
399
|
+
*
|
|
400
|
+
* A client admin's role is `type: "organization"`, which is what the client
|
|
401
|
+
* roles screen (`org/[organization]/roles-permissions.vue`) already lists. This
|
|
402
|
+
* narrows to exactly that. It is a narrowing of this one picker only: no role
|
|
403
|
+
* changes, no permission changes, and the org-less `organization` roles that
|
|
404
|
+
* were already being picked are all still offered.
|
|
405
|
+
*
|
|
406
|
+
* Keying off the selected service instead would empty the dropdown -- no
|
|
407
|
+
* org-less role carries a service type -- so `props.app` is not the filter.
|
|
408
|
+
*/
|
|
391
409
|
const { data: roleData, refresh: refreshRoles } = await useLazyAsyncData(
|
|
392
410
|
"roles",
|
|
393
|
-
() => getRoles({ org: org.value?._id, type:
|
|
411
|
+
() => getRoles({ org: org.value?._id, type: "organization", limit: 50 }),
|
|
394
412
|
{ watch: [() => props.app] }
|
|
395
413
|
)
|
|
396
414
|
|
|
@@ -49,10 +49,16 @@
|
|
|
49
49
|
</label>
|
|
50
50
|
<v-text-field
|
|
51
51
|
v-model="form.tolerance"
|
|
52
|
-
placeholder="60
|
|
52
|
+
placeholder="60"
|
|
53
|
+
suffix="mins"
|
|
54
|
+
inputmode="numeric"
|
|
55
|
+
maxlength="2"
|
|
53
56
|
variant="outlined"
|
|
54
57
|
density="comfortable"
|
|
55
|
-
|
|
58
|
+
:rules="[rules.required, rules.tolerance]"
|
|
59
|
+
hide-details="auto"
|
|
60
|
+
@keydown="allowDigitsOnly"
|
|
61
|
+
@paste="pasteDigitsOnly"
|
|
56
62
|
/>
|
|
57
63
|
</v-col>
|
|
58
64
|
|
|
@@ -163,17 +169,11 @@
|
|
|
163
169
|
Checkpoints
|
|
164
170
|
</v-col>
|
|
165
171
|
<v-col
|
|
166
|
-
cols="
|
|
172
|
+
cols="10"
|
|
167
173
|
class="text-caption screen-hint font-weight-medium pl-4"
|
|
168
174
|
>
|
|
169
175
|
NFC Tag
|
|
170
176
|
</v-col>
|
|
171
|
-
<v-col
|
|
172
|
-
cols="4"
|
|
173
|
-
class="text-caption screen-hint font-weight-medium text-right"
|
|
174
|
-
>
|
|
175
|
-
Travel time
|
|
176
|
-
</v-col>
|
|
177
177
|
</v-row>
|
|
178
178
|
</v-col>
|
|
179
179
|
|
|
@@ -182,12 +182,27 @@
|
|
|
182
182
|
v-for="(checkpoint, index) in form.checkPoints"
|
|
183
183
|
:key="index"
|
|
184
184
|
no-gutters
|
|
185
|
-
class="mb-3 align-center"
|
|
185
|
+
class="mb-3 align-center checkpoint-row"
|
|
186
|
+
:class="{
|
|
187
|
+
'checkpoint-row--dragging': dragIndex === index,
|
|
188
|
+
'checkpoint-row--over':
|
|
189
|
+
dragOverIndex === index && dragIndex !== index,
|
|
190
|
+
}"
|
|
191
|
+
@dragover="onCheckpointDragOver($event, index)"
|
|
192
|
+
@drop="dropCheckpoint($event, index)"
|
|
186
193
|
>
|
|
187
|
-
<v-col cols="1" class="text-body-2">
|
|
194
|
+
<v-col cols="1" class="text-body-2 d-flex align-center">
|
|
195
|
+
<v-icon
|
|
196
|
+
icon="mdi-drag"
|
|
197
|
+
size="small"
|
|
198
|
+
class="drag-handle text-grey mr-1"
|
|
199
|
+
draggable="true"
|
|
200
|
+
@dragstart="startCheckpointDrag($event, index)"
|
|
201
|
+
@dragend="endCheckpointDrag"
|
|
202
|
+
/>
|
|
188
203
|
{{ index + 1 }}
|
|
189
204
|
</v-col>
|
|
190
|
-
<v-col cols="
|
|
205
|
+
<v-col cols="11">
|
|
191
206
|
<v-select
|
|
192
207
|
v-model="checkpoint.nfcTag_id"
|
|
193
208
|
:items="nfcTags"
|
|
@@ -199,35 +214,6 @@
|
|
|
199
214
|
hide-details
|
|
200
215
|
/>
|
|
201
216
|
</v-col>
|
|
202
|
-
<v-col cols="4">
|
|
203
|
-
<v-text-field
|
|
204
|
-
v-model.number="checkpoint.travelTime"
|
|
205
|
-
placeholder="10"
|
|
206
|
-
variant="outlined"
|
|
207
|
-
density="compact"
|
|
208
|
-
hide-details
|
|
209
|
-
suffix="min"
|
|
210
|
-
type="number"
|
|
211
|
-
/>
|
|
212
|
-
</v-col>
|
|
213
|
-
</v-row>
|
|
214
|
-
</v-col>
|
|
215
|
-
|
|
216
|
-
<!-- Summary -->
|
|
217
|
-
<v-col cols="12">
|
|
218
|
-
<v-row no-gutters class="mb-2">
|
|
219
|
-
<v-col cols="6" class="text-body-2 screen-hint">
|
|
220
|
-
Tolerance Balance:
|
|
221
|
-
<span class="font-weight-bold"
|
|
222
|
-
>{{ toleranceBalance }} min</span
|
|
223
|
-
>
|
|
224
|
-
</v-col>
|
|
225
|
-
<v-col cols="6" class="text-body-2 screen-hint text-right">
|
|
226
|
-
Total Travel Time:
|
|
227
|
-
<span class="font-weight-bold"
|
|
228
|
-
>{{ totalTravelTime }} min</span
|
|
229
|
-
>
|
|
230
|
-
</v-col>
|
|
231
217
|
</v-row>
|
|
232
218
|
</v-col>
|
|
233
219
|
</v-row>
|
|
@@ -333,7 +319,6 @@ const loading = ref(false);
|
|
|
333
319
|
|
|
334
320
|
interface Checkpoint {
|
|
335
321
|
nfcTag_id: string;
|
|
336
|
-
travelTime: number;
|
|
337
322
|
}
|
|
338
323
|
|
|
339
324
|
const form = ref({
|
|
@@ -359,10 +344,30 @@ const dayOptions = [
|
|
|
359
344
|
{ title: "Sunday", value: 0 },
|
|
360
345
|
];
|
|
361
346
|
|
|
347
|
+
const MAX_TOLERANCE_MINUTES = 60;
|
|
348
|
+
|
|
362
349
|
const rules = {
|
|
363
350
|
required: (v: any) => !!v || "This field is required",
|
|
351
|
+
tolerance: (v: any) => {
|
|
352
|
+
if (!/^\d+$/.test(String(v ?? ""))) return "Numbers only";
|
|
353
|
+
if (Number(v) > MAX_TOLERANCE_MINUTES)
|
|
354
|
+
return `Maximum is ${MAX_TOLERANCE_MINUTES} minutes (1 hour)`;
|
|
355
|
+
return true;
|
|
356
|
+
},
|
|
364
357
|
};
|
|
365
358
|
|
|
359
|
+
// Tolerance is minutes: keep letters and symbols out as they are typed or
|
|
360
|
+
// pasted. Keys like Backspace, Tab and the arrows have longer names and pass.
|
|
361
|
+
function allowDigitsOnly(event: KeyboardEvent) {
|
|
362
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
363
|
+
if (event.key.length === 1 && !/\d/.test(event.key)) event.preventDefault();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function pasteDigitsOnly(event: ClipboardEvent) {
|
|
367
|
+
const text = event.clipboardData?.getData("text") ?? "";
|
|
368
|
+
if (/\D/.test(text)) event.preventDefault();
|
|
369
|
+
}
|
|
370
|
+
|
|
366
371
|
// Fetch NFC Tags
|
|
367
372
|
const nfcTags = ref<Array<any>>([]);
|
|
368
373
|
const { getAll: getAllNFCTags } = useNFCPatrolTag();
|
|
@@ -385,7 +390,6 @@ function updateCheckpointsCount(count: number) {
|
|
|
385
390
|
for (let i = currentLength; i < count; i++) {
|
|
386
391
|
form.value.checkPoints.push({
|
|
387
392
|
nfcTag_id: "",
|
|
388
|
-
travelTime: 10,
|
|
389
393
|
});
|
|
390
394
|
}
|
|
391
395
|
} else if (count < currentLength) {
|
|
@@ -395,6 +399,43 @@ function updateCheckpointsCount(count: number) {
|
|
|
395
399
|
|
|
396
400
|
}
|
|
397
401
|
|
|
402
|
+
// Drag a checkpoint onto another to swap their positions
|
|
403
|
+
const dragIndex = ref<number | null>(null);
|
|
404
|
+
const dragOverIndex = ref<number | null>(null);
|
|
405
|
+
|
|
406
|
+
function startCheckpointDrag(event: DragEvent, index: number) {
|
|
407
|
+
dragIndex.value = index;
|
|
408
|
+
if (!event.dataTransfer) return;
|
|
409
|
+
event.dataTransfer.effectAllowed = "move";
|
|
410
|
+
// Firefox needs data set to start a drag; a custom type keeps inputs from accepting it on drop
|
|
411
|
+
event.dataTransfer.setData("application/x-nfc-checkpoint", String(index));
|
|
412
|
+
const row = (event.target as HTMLElement).closest(".checkpoint-row");
|
|
413
|
+
if (row) event.dataTransfer.setDragImage(row, 0, 0);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function onCheckpointDragOver(event: DragEvent, index: number) {
|
|
417
|
+
if (dragIndex.value === null) return;
|
|
418
|
+
event.preventDefault();
|
|
419
|
+
dragOverIndex.value = index;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function dropCheckpoint(event: DragEvent, index: number) {
|
|
423
|
+
if (dragIndex.value === null) return;
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
const from = dragIndex.value;
|
|
426
|
+
if (from !== index) {
|
|
427
|
+
const list = [...form.value.checkPoints];
|
|
428
|
+
[list[from], list[index]] = [list[index], list[from]];
|
|
429
|
+
form.value.checkPoints = list;
|
|
430
|
+
}
|
|
431
|
+
endCheckpointDrag();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function endCheckpointDrag() {
|
|
435
|
+
dragIndex.value = null;
|
|
436
|
+
dragOverIndex.value = null;
|
|
437
|
+
}
|
|
438
|
+
|
|
398
439
|
|
|
399
440
|
const hasDuplicateStartTimes = computed(() => {
|
|
400
441
|
const uniqueTimes = new Set(form.value.startTimes);
|
|
@@ -468,20 +509,6 @@ function removeStartTime(index: number) {
|
|
|
468
509
|
startTimeKeys.value.splice(index, 1);
|
|
469
510
|
}
|
|
470
511
|
|
|
471
|
-
// Computed values
|
|
472
|
-
const totalTravelTime = computed(() => {
|
|
473
|
-
const total = form.value.checkPoints.reduce((sum, checkpoint) => {
|
|
474
|
-
return sum + (checkpoint.travelTime || 0);
|
|
475
|
-
}, 0);
|
|
476
|
-
return total;
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
const toleranceBalance = computed(() => {
|
|
480
|
-
const tolerance = parseInt(form.value.tolerance) || 0;
|
|
481
|
-
const balance = tolerance - totalTravelTime.value;
|
|
482
|
-
return balance;
|
|
483
|
-
});
|
|
484
|
-
|
|
485
512
|
// Initialize form
|
|
486
513
|
onMounted(() => {
|
|
487
514
|
fetchNFCTags();
|
|
@@ -516,7 +543,6 @@ async function initializeEditForm(newRoute: any) {
|
|
|
516
543
|
Array.isArray(newRoute.checkPoints) && newRoute.checkPoints.length > 0
|
|
517
544
|
? newRoute.checkPoints.map((cp: any) => ({
|
|
518
545
|
nfcTag_id: cp.nfcTag_id || "",
|
|
519
|
-
travelTime: cp.travelTime || 10
|
|
520
546
|
}))
|
|
521
547
|
: [];
|
|
522
548
|
|
|
@@ -533,6 +559,11 @@ async function initializeEditForm(newRoute: any) {
|
|
|
533
559
|
startTimeKeys.value = startTimes.map(() => Date.now() + Math.random());
|
|
534
560
|
|
|
535
561
|
formInitialized.value = true;
|
|
562
|
+
|
|
563
|
+
// A route saved before the 60-minute limit can be over it: say why Submit
|
|
564
|
+
// is disabled instead of leaving it greyed out with no message.
|
|
565
|
+
await nextTick();
|
|
566
|
+
formRef.value?.validate();
|
|
536
567
|
}
|
|
537
568
|
|
|
538
569
|
watch(
|
|
@@ -592,3 +623,23 @@ async function submit() {
|
|
|
592
623
|
}
|
|
593
624
|
}
|
|
594
625
|
</script>
|
|
626
|
+
|
|
627
|
+
<style scoped>
|
|
628
|
+
.drag-handle {
|
|
629
|
+
cursor: grab;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.checkpoint-row {
|
|
633
|
+
border-radius: 8px;
|
|
634
|
+
transition: background-color 0.15s;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.checkpoint-row--dragging {
|
|
638
|
+
opacity: 0.5;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.checkpoint-row--over {
|
|
642
|
+
background-color: rgba(var(--v-theme-primary), 0.08);
|
|
643
|
+
outline: 1px dashed rgb(var(--v-theme-primary));
|
|
644
|
+
}
|
|
645
|
+
</style>
|
|
@@ -50,8 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
<template #item.tolerance="{ item }">
|
|
52
52
|
<div class="text-center">
|
|
53
|
-
|
|
54
|
-
<div class="text-grey">{{ item.totalTravelTime ? `${item.totalTravelTime} mins` : "--" }}</div>
|
|
53
|
+
{{ item.tolerance ? `${item.tolerance} mins` : "--" }}
|
|
55
54
|
</div>
|
|
56
55
|
</template>
|
|
57
56
|
|
|
@@ -141,17 +140,6 @@
|
|
|
141
140
|
</span>
|
|
142
141
|
</v-col>
|
|
143
142
|
|
|
144
|
-
<v-col
|
|
145
|
-
cols="12"
|
|
146
|
-
class="mb-2"
|
|
147
|
-
v-if="selectedRoute.totalTravelTime"
|
|
148
|
-
>
|
|
149
|
-
Total Travel Time:
|
|
150
|
-
<span class="font-weight-bold">
|
|
151
|
-
{{ selectedRoute.totalTravelTime }} mins
|
|
152
|
-
</span>
|
|
153
|
-
</v-col>
|
|
154
|
-
|
|
155
143
|
<v-col cols="12" class="mb-2" v-if="selectedRoute.repeatDays">
|
|
156
144
|
Repeat Every:
|
|
157
145
|
<span class="font-weight-bold">
|
|
@@ -178,7 +166,7 @@
|
|
|
178
166
|
Checkpoints:
|
|
179
167
|
<div class="font-weight-bold">
|
|
180
168
|
<div v-for="(cp, idx) in selectedRoute.checkpoints" :key="idx" class="my-1">
|
|
181
|
-
{{ idx + 1 }}. NFC Tag ID: {{ cp.nfcTagId }}
|
|
169
|
+
{{ idx + 1 }}. NFC Tag ID: {{ cp.nfcTagId }}
|
|
182
170
|
</div>
|
|
183
171
|
</div>
|
|
184
172
|
</v-col>
|
|
@@ -335,7 +323,7 @@ const props = defineProps({
|
|
|
335
323
|
{ title: "Route Name", value: "routeName", align: "start" },
|
|
336
324
|
{ title: "Checkpoints", value: "numberOfCheckpoints", align: "center" },
|
|
337
325
|
{
|
|
338
|
-
title: "Tolerance (mins)
|
|
326
|
+
title: "Tolerance (mins)",
|
|
339
327
|
value: "tolerance",
|
|
340
328
|
align: "center",
|
|
341
329
|
sortable: false
|
|
@@ -395,7 +383,6 @@ watchEffect(() => {
|
|
|
395
383
|
name: item.name,
|
|
396
384
|
checkPointNumber: item.checkPointNumber,
|
|
397
385
|
tolerance: item.tolerance,
|
|
398
|
-
totalTravelTime: item.totalTravelTime,
|
|
399
386
|
days: item.days,
|
|
400
387
|
startTimes: item.startTimes || [],
|
|
401
388
|
checkPoints: item.checkPoints || [],
|
|
@@ -412,7 +399,6 @@ watchEffect(() => {
|
|
|
412
399
|
}).join(', ') || '--',
|
|
413
400
|
checkpoints: item.checkPoints?.map((cp: any) => ({
|
|
414
401
|
nfcTagId: cp.nfcTag_id,
|
|
415
|
-
travelTime: cp.travelTime
|
|
416
402
|
})) || []
|
|
417
403
|
})) || [];
|
|
418
404
|
|
|
@@ -516,7 +502,6 @@ const selectedRoute = ref<any>({
|
|
|
516
502
|
checkPointNumber: 5,
|
|
517
503
|
numberOfCheckpoints: 5,
|
|
518
504
|
tolerance: undefined,
|
|
519
|
-
totalTravelTime: undefined,
|
|
520
505
|
days: [],
|
|
521
506
|
repeatDays: "",
|
|
522
507
|
startTimes: [],
|
|
@@ -25,10 +25,6 @@
|
|
|
25
25
|
<th>Security Personnel</th>
|
|
26
26
|
<th>Signature</th>
|
|
27
27
|
<th>Start - End Time</th>
|
|
28
|
-
<th>
|
|
29
|
-
Travel time<br />
|
|
30
|
-
<span class="text-caption text-medium-emphasis">Actual/Expected</span>
|
|
31
|
-
</th>
|
|
32
28
|
<th>Status</th>
|
|
33
29
|
</tr>
|
|
34
30
|
</thead>
|
|
@@ -58,10 +54,6 @@
|
|
|
58
54
|
<v-icon size="small" class="mr-1">mdi-clock-outline</v-icon>
|
|
59
55
|
{{ activity.endTime }}
|
|
60
56
|
</td>
|
|
61
|
-
<td>
|
|
62
|
-
<v-icon size="small" class="mr-1">mdi-clock-outline</v-icon>
|
|
63
|
-
{{ activity.actualTime }} / {{ activity.expectedTime }}
|
|
64
|
-
</td>
|
|
65
57
|
<td>
|
|
66
58
|
<v-chip :color="statusColor(activity.status)" size="small" variant="flat">
|
|
67
59
|
{{ activity.status }}
|
|
@@ -36,6 +36,14 @@
|
|
|
36
36
|
>
|
|
37
37
|
Add
|
|
38
38
|
</AppButton>
|
|
39
|
+
<AppButton
|
|
40
|
+
v-if="canInviteServiceProvider && siteId"
|
|
41
|
+
variant="ghost"
|
|
42
|
+
data-test="sp-provider-changes"
|
|
43
|
+
@click="providerChange?.openHistory()"
|
|
44
|
+
>
|
|
45
|
+
Provider changes
|
|
46
|
+
</AppButton>
|
|
39
47
|
<AppButton
|
|
40
48
|
v-if="canInviteServiceProvider"
|
|
41
49
|
@click="
|
|
@@ -202,6 +210,17 @@
|
|
|
202
210
|
the engagement itself, not a stored service-providers record, so
|
|
203
211
|
the In-Active move (which acts on that record) does not apply.
|
|
204
212
|
-->
|
|
213
|
+
<v-menu v-else-if="item.source === 'engagement' && canChangeProvider(item)" location="bottom end">
|
|
214
|
+
<template #activator="{ props: menuProps }">
|
|
215
|
+
<v-btn v-bind="menuProps" icon variant="text" density="comfortable" aria-label="Provider actions">
|
|
216
|
+
<v-icon>mdi-dots-vertical</v-icon>
|
|
217
|
+
</v-btn>
|
|
218
|
+
</template>
|
|
219
|
+
<v-list density="compact" min-width="200">
|
|
220
|
+
<v-list-item title="View provider" @click="openProvider(item)" />
|
|
221
|
+
<v-list-item title="Change or end provider" data-test="sp-change-provider" @click="openProviderChange(item)" />
|
|
222
|
+
</v-list>
|
|
223
|
+
</v-menu>
|
|
205
224
|
<v-btn
|
|
206
225
|
v-else-if="item.source === 'engagement'"
|
|
207
226
|
icon
|
|
@@ -230,6 +249,12 @@
|
|
|
230
249
|
title="View Members"
|
|
231
250
|
@click="openView(item)"
|
|
232
251
|
/>
|
|
252
|
+
<v-list-item
|
|
253
|
+
v-if="canChangeProvider(item)"
|
|
254
|
+
title="Change or end provider"
|
|
255
|
+
data-test="sp-change-provider"
|
|
256
|
+
@click="openProviderChange(item)"
|
|
257
|
+
/>
|
|
233
258
|
<v-list-item
|
|
234
259
|
:title="rowActionLabel(item)"
|
|
235
260
|
:class="{
|
|
@@ -720,6 +745,15 @@
|
|
|
720
745
|
</v-dialog>
|
|
721
746
|
|
|
722
747
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
748
|
+
|
|
749
|
+
<SiteProviderChange
|
|
750
|
+
v-if="siteId"
|
|
751
|
+
ref="providerChange"
|
|
752
|
+
:site-id="siteId"
|
|
753
|
+
:site-name="siteName || site?.name || ''"
|
|
754
|
+
:owner-org-id="String(site?.orgId || '')"
|
|
755
|
+
@changed="loadList()"
|
|
756
|
+
/>
|
|
723
757
|
</template>
|
|
724
758
|
|
|
725
759
|
<script lang="ts" setup>
|
|
@@ -998,6 +1032,20 @@ const { data: site } = await useLazyAsyncData(
|
|
|
998
1032
|
|
|
999
1033
|
const searchText = ref("");
|
|
1000
1034
|
|
|
1035
|
+
// Change or end a provider at this site (owner 2026-10-05). The server decides
|
|
1036
|
+
// who may; the menu only offers it to people who may invite providers, on a
|
|
1037
|
+
// live provider row that names its company.
|
|
1038
|
+
const providerChange = ref<{
|
|
1039
|
+
openChange: (row: { from: string; fromName: string }) => void;
|
|
1040
|
+
openHistory: () => void;
|
|
1041
|
+
} | null>(null);
|
|
1042
|
+
function canChangeProvider(item: TableRow) {
|
|
1043
|
+
return Boolean(canInviteServiceProvider.value && props.siteId && listTab.value === "active" && item.serviceProviderOrgId);
|
|
1044
|
+
}
|
|
1045
|
+
function openProviderChange(item: TableRow) {
|
|
1046
|
+
providerChange.value?.openChange({ from: item.serviceProviderOrgId, fromName: item.companyName });
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1001
1049
|
const createDialog = ref(false);
|
|
1002
1050
|
const createInviteDialog = ref(false);
|
|
1003
1051
|
// const loadingInviteRoles = ref(false);
|