@7365admin1/layer-common 3.2.1 → 3.2.2-staging.101
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/.changeset/camera-wall-gated-endpoint.md +47 -0
- package/.changeset/camera-wall-plain-english.md +60 -0
- package/.changeset/camera-wall-selection-and-guidance.md +65 -0
- package/.changeset/dark-primary-contrast.md +39 -0
- package/.changeset/dashboard-dark-theme-contrast.md +15 -0
- package/.changeset/notification-preferences.md +17 -0
- package/.changeset/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/.changeset/service-provider-invitation-actions.md +28 -0
- package/assets/css/primitives.css +680 -0
- package/assets/css/screens.css +541 -0
- package/assets/css/shell.css +258 -0
- package/assets/css/tokens.css +220 -0
- package/components/AccessCardQrTagging.vue +1 -1
- package/components/AccessManagement.vue +53 -72
- package/components/AppButton.vue +52 -0
- package/components/AppCard.vue +20 -0
- package/components/AppDateField.vue +35 -0
- package/components/AppField.vue +51 -0
- package/components/AppKpiTile.vue +65 -0
- package/components/AppSegmented.vue +38 -0
- package/components/AppSelect.vue +203 -0
- package/components/AreaChecklistHistoryLogs.vue +51 -44
- package/components/AreaChecklistHistoryMain.vue +6 -2
- package/components/AreaMain.vue +9 -17
- package/components/AttendanceMain.vue +4 -3
- package/components/Avatar/Main.vue +54 -2
- package/components/BillingMain.vue +26 -30
- package/components/BuildingManagement/buildings.vue +27 -38
- package/components/BuildingManagement/units.vue +26 -42
- package/components/BulletinBoardManagement.vue +23 -10
- package/components/CameraWall.vue +1034 -0
- package/components/CameraWallTile.vue +818 -0
- package/components/CardToolbar.vue +37 -0
- package/components/CleaningScheduleMain.vue +29 -46
- package/components/ClientMain.vue +90 -78
- package/components/DashboardEmptyState.vue +11 -1
- package/components/DashboardMain.vue +349 -275
- package/components/DashboardPanel.vue +1 -1
- package/components/DocumentManagement.vue +129 -93
- package/components/EntryPassInformation.vue +4 -4
- package/components/EntryPassMain.vue +49 -27
- package/components/EquipmentItemMain.vue +4 -6
- package/components/EquipmentManagementMain.vue +9 -7
- package/components/FeedbackMain.vue +42 -56
- package/components/FormDialog.vue +60 -21
- package/components/HidAccessLogDashboard.vue +119 -44
- package/components/HidCameraCaptureDialog.vue +199 -0
- package/components/HidIntercomManagement.vue +825 -202
- package/components/HidQrCodeConfiguration.vue +927 -93
- package/components/HidServiceSettingsPanel.vue +20 -1
- package/components/HidUserEnrollment.vue +454 -106
- package/components/InvitationMain.vue +46 -61
- package/components/Layout/Header.vue +289 -37
- package/components/Layout/NavigationDrawer.vue +340 -30
- package/components/ManageChecklistMain.vue +17 -17
- package/components/MemberMain.vue +79 -60
- package/components/MyAttendanceMain.vue +3 -5
- package/components/NavigationItem.vue +9 -0
- package/components/Nfc/NFCPatrolRouteMain.vue +116 -103
- package/components/NotificationSettings.vue +361 -0
- package/components/OnlineFormConfigurationForm.vue +620 -224
- package/components/OnlineFormFill.vue +36 -8
- package/components/OnlineFormsConfiguration.vue +7 -3
- package/components/PageHeader.vue +43 -0
- package/components/PassInformation.vue +104 -45
- package/components/QrTemplate/CreditCardLandscape.vue +19 -12
- package/components/QrTemplate/PrintDialog.vue +209 -196
- package/components/RolePermissionMain.vue +184 -91
- package/components/ScanVisitorQRCode.vue +35 -7
- package/components/ScheduleAreaMain.vue +15 -49
- package/components/ScheduleTaskMain.vue +23 -26
- package/components/ScheduleTastTicketMain.vue +18 -43
- package/components/ServiceProviderInvitationPrompt.vue +150 -0
- package/components/ServiceProviderMain.vue +346 -157
- package/components/SiteSettings.vue +25 -10
- package/components/SiteSettingsWorkOrder.vue +61 -30
- package/components/SpecificAttr.vue +10 -2
- package/components/StatusChip.vue +93 -0
- package/components/StockCard.vue +17 -12
- package/components/SwitchContext.vue +33 -1
- package/components/TableHygiene.vue +29 -46
- package/components/TableList.vue +1 -1
- package/components/TableListSecondary.vue +1 -1
- package/components/TableMain.vue +161 -77
- package/components/TableV3.vue +61 -66
- package/components/UnitMain.vue +9 -17
- package/components/VehicleManagement.vue +20 -9
- package/components/VisitorForm.vue +294 -78
- package/components/VisitorManagement.vue +221 -71
- package/components/VisitorSocketPopUp.vue +56 -2
- package/components/WorkOrder/Main.vue +42 -51
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +151 -0
- package/composables/useHidNavigation.ts +0 -7
- package/composables/useLocalAuth.ts +9 -36
- package/composables/useNotificationPreference.ts +87 -0
- package/composables/useOnlineFormPages.ts +2 -0
- package/composables/useOrg.ts +1 -1
- package/composables/useRole.ts +22 -0
- package/composables/useServiceProviderInvitation.ts +145 -0
- package/composables/useSipWebPhone.ts +336 -0
- package/composables/useSiteSettings.ts +50 -0
- package/composables/useThemePreference.ts +63 -0
- package/composables/useVisitor.ts +6 -1
- package/composables/useVisitorSocket.ts +26 -0
- package/composables/useWebUsb.ts +127 -37
- package/nuxt.config.ts +31 -0
- package/package.json +4 -1
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/pages/notification-settings.vue +19 -0
- package/plugins/secure-member.client.ts +21 -56
- package/plugins/vuetify.ts +68 -9
- package/test/visitor-socket.test.mjs +36 -0
- package/types/org.d.ts +2 -0
- package/types/site.d.ts +71 -0
- package/utils/camera-wall.test.ts +571 -0
- package/utils/camera-wall.ts +926 -0
- package/utils/nav-sections.test.ts +190 -0
- package/utils/nav-sections.ts +151 -0
- package/utils/status.test.ts +77 -0
- package/utils/status.ts +90 -0
- package/utils/theme.test.ts +534 -0
- package/utils/theme.ts +294 -0
- package/components/HidIdentityMapping.vue +0 -975
- package/middleware/member.ts +0 -4
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +0 -23
|
@@ -1,112 +1,91 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<v-card
|
|
61
|
-
width="100%"
|
|
62
|
-
variant="outlined"
|
|
63
|
-
border="thin"
|
|
64
|
-
rounded="lg"
|
|
65
|
-
:loading="loading"
|
|
66
|
-
>
|
|
67
|
-
<div class="sp-table-header">
|
|
68
|
-
<v-toolbar
|
|
69
|
-
density="compact"
|
|
70
|
-
color="grey-lighten-4"
|
|
71
|
-
flat
|
|
72
|
-
class="sp-toolbar-top px-2"
|
|
73
|
-
>
|
|
74
|
-
<v-btn
|
|
75
|
-
icon
|
|
76
|
-
variant="text"
|
|
77
|
-
density="comfortable"
|
|
78
|
-
@click="loadList()"
|
|
79
|
-
>
|
|
80
|
-
<v-icon>mdi-refresh</v-icon>
|
|
81
|
-
</v-btn>
|
|
82
|
-
|
|
83
|
-
<v-spacer />
|
|
84
|
-
|
|
85
|
-
<template v-if="canViewServiceProviders">
|
|
86
|
-
<span class="mr-2 text-caption text-medium-emphasis">{{
|
|
87
|
-
pageRange
|
|
88
|
-
}}</span>
|
|
89
|
-
<local-pagination
|
|
90
|
-
v-model="page"
|
|
91
|
-
:length="pages"
|
|
92
|
-
@update:value="loadList()"
|
|
93
|
-
/>
|
|
94
|
-
</template>
|
|
95
|
-
</v-toolbar>
|
|
96
|
-
|
|
97
|
-
<v-tabs
|
|
98
|
-
v-model="listTab"
|
|
99
|
-
color="primary"
|
|
100
|
-
density="comfortable"
|
|
101
|
-
class="sp-tabs"
|
|
102
|
-
align-tabs="start"
|
|
2
|
+
<div>
|
|
3
|
+
<!--
|
|
4
|
+
THE DESIGN'S PAGE SCAFFOLD (same shape as Members).
|
|
5
|
+
|
|
6
|
+
The screen had no title at all, its primary actions sat top-LEFT, the
|
|
7
|
+
search box floated outside the card, and the refresh + pager sat on one
|
|
8
|
+
row with the tabs stacked on a second row below. The design draws a 22px
|
|
9
|
+
title with the actions on the right of that line, then ONE row inside the
|
|
10
|
+
card: tabs left, search / refresh / count / pager right.
|
|
11
|
+
|
|
12
|
+
Placement only. The same buttons open the same dialogs under the same
|
|
13
|
+
permission conditions, the same three tabs load the same three lists, the
|
|
14
|
+
same search feeds the same debounced `loadList()`, and the table, its
|
|
15
|
+
columns and its row menu are untouched.
|
|
16
|
+
-->
|
|
17
|
+
<PageHeader title="Service Providers">
|
|
18
|
+
<template #actions>
|
|
19
|
+
<AppButton
|
|
20
|
+
v-if="showBillingButton"
|
|
21
|
+
variant="ghost"
|
|
22
|
+
@click="
|
|
23
|
+
useRouter().push({
|
|
24
|
+
name: 'org-site-service-provider-mgmt-billing',
|
|
25
|
+
})
|
|
26
|
+
"
|
|
27
|
+
>
|
|
28
|
+
Billing
|
|
29
|
+
</AppButton>
|
|
30
|
+
<AppButton
|
|
31
|
+
v-if="showAddButton && canAddServiceProvider"
|
|
32
|
+
variant="ghost"
|
|
33
|
+
@click="(createDialog = true), setServiceProvider({ mode: 'add' })"
|
|
34
|
+
>
|
|
35
|
+
Add
|
|
36
|
+
</AppButton>
|
|
37
|
+
<AppButton
|
|
38
|
+
v-if="canInviteServiceProvider"
|
|
39
|
+
@click="
|
|
40
|
+
(createInviteDialog = true), setServiceProvider({ mode: 'invite' })
|
|
41
|
+
"
|
|
42
|
+
>
|
|
43
|
+
Invite
|
|
44
|
+
</AppButton>
|
|
45
|
+
</template>
|
|
46
|
+
</PageHeader>
|
|
47
|
+
|
|
48
|
+
<AppCard class="table-card">
|
|
49
|
+
<CardToolbar :count="canViewServiceProviders ? pageRange : ''">
|
|
50
|
+
<template #tabs>
|
|
51
|
+
<button
|
|
52
|
+
v-for="tab in listTabs"
|
|
53
|
+
:key="tab.value"
|
|
54
|
+
type="button"
|
|
55
|
+
role="tab"
|
|
56
|
+
class="app-tab"
|
|
57
|
+
:class="{ 'app-tab--active': listTab === tab.value }"
|
|
58
|
+
:aria-selected="listTab === tab.value"
|
|
59
|
+
@click="listTab = tab.value"
|
|
103
60
|
>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
61
|
+
{{ tab.label }}
|
|
62
|
+
</button>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<template #right>
|
|
66
|
+
<AppField
|
|
67
|
+
v-model="searchText"
|
|
68
|
+
search
|
|
69
|
+
compact
|
|
70
|
+
placeholder="Search"
|
|
71
|
+
klass="app-toolbar__search"
|
|
72
|
+
/>
|
|
73
|
+
<AppButton
|
|
74
|
+
variant="icon"
|
|
75
|
+
icon="mdi-refresh"
|
|
76
|
+
aria-label="Refresh"
|
|
77
|
+
@click="loadList()"
|
|
78
|
+
/>
|
|
79
|
+
<local-pagination
|
|
80
|
+
v-if="canViewServiceProviders"
|
|
81
|
+
v-model="page"
|
|
82
|
+
:length="pages"
|
|
83
|
+
@update:value="loadList()"
|
|
84
|
+
/>
|
|
85
|
+
</template>
|
|
86
|
+
</CardToolbar>
|
|
87
|
+
|
|
88
|
+
<div class="app-table-scroll">
|
|
110
89
|
<v-data-table
|
|
111
90
|
v-if="canViewServiceProviders"
|
|
112
91
|
:headers="headers"
|
|
@@ -115,6 +94,7 @@
|
|
|
115
94
|
:items-per-page="pageSize"
|
|
116
95
|
fixed-header
|
|
117
96
|
hide-default-footer
|
|
97
|
+
:loading="loading"
|
|
118
98
|
class="sp-table"
|
|
119
99
|
style="max-height: calc(100vh - 280px)"
|
|
120
100
|
>
|
|
@@ -137,17 +117,65 @@
|
|
|
137
117
|
<span class="text-body-2">{{ item.members }}</span>
|
|
138
118
|
</template>
|
|
139
119
|
<template #item.status="{ item }">
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
120
|
+
<!--
|
|
121
|
+
Invitation rows use a FILLED chip, not a tonal one. A tonal chip
|
|
122
|
+
paints the status colour as TEXT, and measured on white the
|
|
123
|
+
warning and info tokens come out at 2.4:1 and 3.1:1 — under the
|
|
124
|
+
4.5:1 a 12px label needs. Filled puts the colour in the
|
|
125
|
+
background and lets Vuetify pick a readable foreground for it.
|
|
126
|
+
-->
|
|
127
|
+
<StatusChip
|
|
144
128
|
class="text-none"
|
|
129
|
+
:status="item.statusRaw || item.status"
|
|
130
|
+
:label="item.status"
|
|
131
|
+
/>
|
|
132
|
+
<div
|
|
133
|
+
v-if="item.rejectionReason"
|
|
134
|
+
class="text-caption text-medium-emphasis mt-1 sp-reject-reason"
|
|
145
135
|
>
|
|
146
|
-
{{ item.
|
|
147
|
-
</
|
|
136
|
+
Seven365 said: {{ item.rejectionReason }}
|
|
137
|
+
</div>
|
|
148
138
|
</template>
|
|
149
139
|
<template #item.actions="{ item }">
|
|
150
|
-
|
|
140
|
+
<!--
|
|
141
|
+
Invitation rows. The menu used to be drawn only for
|
|
142
|
+
`source === 'provider'`, so a pending invitation showed no
|
|
143
|
+
actions at all and the cancel already wired up behind it was
|
|
144
|
+
unreachable.
|
|
145
|
+
-->
|
|
146
|
+
<v-menu v-if="item.source === 'invite'" location="bottom end">
|
|
147
|
+
<template #activator="{ props: menuProps }">
|
|
148
|
+
<v-btn
|
|
149
|
+
v-bind="menuProps"
|
|
150
|
+
icon
|
|
151
|
+
variant="text"
|
|
152
|
+
density="comfortable"
|
|
153
|
+
aria-label="Invitation actions"
|
|
154
|
+
>
|
|
155
|
+
<v-icon>mdi-dots-vertical</v-icon>
|
|
156
|
+
</v-btn>
|
|
157
|
+
</template>
|
|
158
|
+
<v-list density="compact" min-width="220">
|
|
159
|
+
<v-list-item
|
|
160
|
+
title="Cancel invitation"
|
|
161
|
+
:disabled="!canCancelInvite(item)"
|
|
162
|
+
:subtitle="canCancelInvite(item) ? undefined : 'Only while it is still waiting.'"
|
|
163
|
+
@click="openInviteAction(item, 'cancel')"
|
|
164
|
+
/>
|
|
165
|
+
<v-list-item
|
|
166
|
+
title="Resend invitation"
|
|
167
|
+
:disabled="!canResendInvite(item)"
|
|
168
|
+
:subtitle="resendHint(item)"
|
|
169
|
+
@click="openInviteAction(item, 'resend')"
|
|
170
|
+
/>
|
|
171
|
+
<v-list-item
|
|
172
|
+
title="Delete invitation"
|
|
173
|
+
base-color="error"
|
|
174
|
+
@click="openInviteAction(item, 'delete')"
|
|
175
|
+
/>
|
|
176
|
+
</v-list>
|
|
177
|
+
</v-menu>
|
|
178
|
+
<v-menu v-else-if="item.source === 'provider'" location="bottom end">
|
|
151
179
|
<template #activator="{ props: menuProps }">
|
|
152
180
|
<v-btn
|
|
153
181
|
v-bind="menuProps"
|
|
@@ -178,12 +206,16 @@
|
|
|
178
206
|
</v-menu>
|
|
179
207
|
</template>
|
|
180
208
|
</v-data-table>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div
|
|
212
|
+
v-if="!canViewServiceProviders"
|
|
213
|
+
class="pa-4 text-body-2 text-medium-emphasis"
|
|
214
|
+
>
|
|
215
|
+
You do not have permission to view service providers.
|
|
216
|
+
</div>
|
|
217
|
+
</AppCard>
|
|
218
|
+
</div>
|
|
187
219
|
|
|
188
220
|
<v-dialog v-model="viewDialog" max-width="514">
|
|
189
221
|
<v-card rounded="lg" class="sp-members-card">
|
|
@@ -284,6 +316,56 @@
|
|
|
284
316
|
</v-card>
|
|
285
317
|
</v-dialog>
|
|
286
318
|
|
|
319
|
+
<!-- Cancel / Resend / Delete an invitation. Same shape as the provider
|
|
320
|
+
dialog above so the two read as one product. -->
|
|
321
|
+
<v-dialog v-model="inviteDialog" max-width="728" persistent>
|
|
322
|
+
<v-card rounded="lg" class="sp-status-card">
|
|
323
|
+
<v-card-title class="sp-status-title">
|
|
324
|
+
{{ inviteDialogTitle }}
|
|
325
|
+
</v-card-title>
|
|
326
|
+
|
|
327
|
+
<v-card-text class="sp-status-body">
|
|
328
|
+
<div class="sp-status-question">
|
|
329
|
+
{{ inviteDialogQuestion }}
|
|
330
|
+
</div>
|
|
331
|
+
<div class="sp-status-copy">
|
|
332
|
+
{{ inviteDialogCopy }}
|
|
333
|
+
</div>
|
|
334
|
+
</v-card-text>
|
|
335
|
+
|
|
336
|
+
<v-divider />
|
|
337
|
+
|
|
338
|
+
<v-card-actions class="pa-0">
|
|
339
|
+
<v-row no-gutters class="fill-height">
|
|
340
|
+
<v-col cols="6">
|
|
341
|
+
<v-btn
|
|
342
|
+
block
|
|
343
|
+
variant="flat"
|
|
344
|
+
height="82"
|
|
345
|
+
class="text-none sp-status-cancel"
|
|
346
|
+
:disabled="inviteWorking"
|
|
347
|
+
@click="closeInviteDialog"
|
|
348
|
+
>
|
|
349
|
+
Keep it
|
|
350
|
+
</v-btn>
|
|
351
|
+
</v-col>
|
|
352
|
+
<v-col cols="6">
|
|
353
|
+
<v-btn
|
|
354
|
+
block
|
|
355
|
+
variant="flat"
|
|
356
|
+
height="82"
|
|
357
|
+
class="text-none sp-status-confirm"
|
|
358
|
+
:loading="inviteWorking"
|
|
359
|
+
@click="confirmInviteAction"
|
|
360
|
+
>
|
|
361
|
+
{{ inviteConfirmLabel }}
|
|
362
|
+
</v-btn>
|
|
363
|
+
</v-col>
|
|
364
|
+
</v-row>
|
|
365
|
+
</v-card-actions>
|
|
366
|
+
</v-card>
|
|
367
|
+
</v-dialog>
|
|
368
|
+
|
|
287
369
|
<v-dialog v-model="createDialog" width="500" persistent>
|
|
288
370
|
<v-card width="100%">
|
|
289
371
|
<v-toolbar>
|
|
@@ -416,7 +498,7 @@
|
|
|
416
498
|
|
|
417
499
|
<v-dialog v-model="createInviteDialog" width="580" persistent>
|
|
418
500
|
<v-card width="100%" rounded="lg" class="sp-invite-card">
|
|
419
|
-
<v-toolbar flat color="
|
|
501
|
+
<v-toolbar flat color="transparent" class="table-card__toolbar" height="76">
|
|
420
502
|
<v-row no-gutters class="fill-height px-6" align="center">
|
|
421
503
|
<span class="font-weight-bold text-h5">
|
|
422
504
|
Invite Service Provider
|
|
@@ -582,6 +664,11 @@ type TableRow = {
|
|
|
582
664
|
members: string;
|
|
583
665
|
status: string;
|
|
584
666
|
source: "provider" | "invite";
|
|
667
|
+
/** the stored status, for deciding which actions a row may offer */
|
|
668
|
+
statusRaw?: string;
|
|
669
|
+
/** shown under a "Not approved" row, in Seven365's own words */
|
|
670
|
+
rejectionReason?: string;
|
|
671
|
+
siteName?: string;
|
|
585
672
|
};
|
|
586
673
|
|
|
587
674
|
type ServiceProviderMemberRow = {
|
|
@@ -692,6 +779,12 @@ const { requiredRule, emailRule, debounce } = useUtils();
|
|
|
692
779
|
const { getUserByEmail } = useUser();
|
|
693
780
|
|
|
694
781
|
const listTab = ref<ListTab>("active");
|
|
782
|
+
/** The same three tabs, in the same order, with the same labels. */
|
|
783
|
+
const listTabs: Array<{ value: ListTab; label: string }> = [
|
|
784
|
+
{ value: "active", label: "Service Providers" },
|
|
785
|
+
{ value: "pending", label: "Pending" },
|
|
786
|
+
{ value: "inactive", label: "In-Active" },
|
|
787
|
+
];
|
|
695
788
|
const page = ref(1);
|
|
696
789
|
const pages = ref(0);
|
|
697
790
|
const pageRange = ref("-- - -- of --");
|
|
@@ -739,6 +832,13 @@ const { getAll: getAllMembers, getAllByUserId } = useMember();
|
|
|
739
832
|
const { getVerifications, cancelUserInvitation, getVerificationByEmail } =
|
|
740
833
|
useVerification();
|
|
741
834
|
|
|
835
|
+
const {
|
|
836
|
+
getInvitations,
|
|
837
|
+
cancelInvitation,
|
|
838
|
+
resendInvitation,
|
|
839
|
+
deleteInvitation,
|
|
840
|
+
} = useServiceProviderInvitation();
|
|
841
|
+
|
|
742
842
|
const existingAccount = ref<any>(null);
|
|
743
843
|
const checkingExistingAccount = ref(false);
|
|
744
844
|
// const { getRoles } = useRole();
|
|
@@ -877,6 +977,109 @@ function canViewMembers(row: TableRow) {
|
|
|
877
977
|
return row.source === "provider";
|
|
878
978
|
}
|
|
879
979
|
|
|
980
|
+
// --- invitation actions (Cancel / Resend / Delete) ---------------------------
|
|
981
|
+
//
|
|
982
|
+
// Each one confirms first, and each one leaves an audit line on the server —
|
|
983
|
+
// who did it, when, and what. Delete is a SOFT delete: the row leaves this
|
|
984
|
+
// list, Seven365 keeps the record and its history.
|
|
985
|
+
|
|
986
|
+
type InviteAction = "cancel" | "resend" | "delete";
|
|
987
|
+
|
|
988
|
+
const inviteDialog = ref(false);
|
|
989
|
+
const inviteWorking = ref(false);
|
|
990
|
+
const inviteActionRow = ref<TableRow | null>(null);
|
|
991
|
+
const inviteAction = ref<InviteAction>("cancel");
|
|
992
|
+
|
|
993
|
+
function canCancelInvite(row: TableRow) {
|
|
994
|
+
return ["awaiting-approval", "pending"].includes(row.statusRaw ?? "");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function canResendInvite(row: TableRow) {
|
|
998
|
+
return (row.statusRaw ?? "") === "pending";
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function resendHint(row: TableRow) {
|
|
1002
|
+
if (canResendInvite(row)) return undefined;
|
|
1003
|
+
return (row.statusRaw ?? "") === "awaiting-approval"
|
|
1004
|
+
? "You can resend once Seven365 has approved it."
|
|
1005
|
+
: "Only an approved invitation can be sent again.";
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const inviteDialogTitle = computed(() => {
|
|
1009
|
+
if (inviteAction.value === "resend") return "Resend Invitation";
|
|
1010
|
+
if (inviteAction.value === "delete") return "Delete Invitation";
|
|
1011
|
+
return "Cancel Invitation";
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
const inviteDialogQuestion = computed(() => {
|
|
1015
|
+
const row = inviteActionRow.value;
|
|
1016
|
+
const who = row?.companyName && row.companyName !== "-" ? row.companyName : row?.email;
|
|
1017
|
+
const site = row?.siteName ? ` for ${row.siteName}` : "";
|
|
1018
|
+
|
|
1019
|
+
if (inviteAction.value === "resend") {
|
|
1020
|
+
return `Send this invitation again to ${row?.email}?`;
|
|
1021
|
+
}
|
|
1022
|
+
if (inviteAction.value === "delete") {
|
|
1023
|
+
return "Delete this invitation permanently?";
|
|
1024
|
+
}
|
|
1025
|
+
return `Cancel this invitation to ${who}${site}?`;
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
const inviteDialogCopy = computed(() => {
|
|
1029
|
+
if (inviteAction.value === "resend") {
|
|
1030
|
+
return "They will get a fresh message and another 3 days to accept. This does not create a second invitation.";
|
|
1031
|
+
}
|
|
1032
|
+
if (inviteAction.value === "delete") {
|
|
1033
|
+
return "It will disappear from this list. Seven365 keeps the record of who sent it and what happened to it.";
|
|
1034
|
+
}
|
|
1035
|
+
return "They will not be able to accept it. You can send a new one later.";
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
const inviteConfirmLabel = computed(() => {
|
|
1039
|
+
if (inviteAction.value === "resend") return "Resend";
|
|
1040
|
+
if (inviteAction.value === "delete") return "Delete";
|
|
1041
|
+
return "Cancel invitation";
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
function openInviteAction(row: TableRow, action: InviteAction) {
|
|
1045
|
+
if (action === "cancel" && !canCancelInvite(row)) return;
|
|
1046
|
+
if (action === "resend" && !canResendInvite(row)) return;
|
|
1047
|
+
inviteActionRow.value = row;
|
|
1048
|
+
inviteAction.value = action;
|
|
1049
|
+
inviteDialog.value = true;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function closeInviteDialog() {
|
|
1053
|
+
if (inviteWorking.value) return;
|
|
1054
|
+
inviteDialog.value = false;
|
|
1055
|
+
inviteActionRow.value = null;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async function confirmInviteAction() {
|
|
1059
|
+
const row = inviteActionRow.value;
|
|
1060
|
+
if (!row) return;
|
|
1061
|
+
|
|
1062
|
+
inviteWorking.value = true;
|
|
1063
|
+
try {
|
|
1064
|
+
const run =
|
|
1065
|
+
inviteAction.value === "resend"
|
|
1066
|
+
? resendInvitation
|
|
1067
|
+
: inviteAction.value === "delete"
|
|
1068
|
+
? deleteInvitation
|
|
1069
|
+
: cancelInvitation;
|
|
1070
|
+
|
|
1071
|
+
const result = await run(row._id);
|
|
1072
|
+
showMessage(result?.message ?? "Done.", "success");
|
|
1073
|
+
inviteDialog.value = false;
|
|
1074
|
+
inviteActionRow.value = null;
|
|
1075
|
+
await loadList();
|
|
1076
|
+
} catch (error: any) {
|
|
1077
|
+
showMessage(errorConverter(error), "error");
|
|
1078
|
+
} finally {
|
|
1079
|
+
inviteWorking.value = false;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
880
1083
|
const statusDialog = ref(false);
|
|
881
1084
|
const statusUpdating = ref(false);
|
|
882
1085
|
const statusActionRow = ref<TableRow | null>(null);
|
|
@@ -908,7 +1111,11 @@ function rowActionLabel(row: TableRow) {
|
|
|
908
1111
|
|
|
909
1112
|
function typeDisplay(type: string, nature?: string) {
|
|
910
1113
|
if (nature && String(nature).trim()) return String(nature);
|
|
911
|
-
|
|
1114
|
+
// useLocal returns a plain array; reading `.value` off it was always
|
|
1115
|
+
// undefined, so every row fell through to the raw stored slug
|
|
1116
|
+
// ("security_agency") instead of "Security".
|
|
1117
|
+
const source = natureOfBusinessV3 as any;
|
|
1118
|
+
const list = (Array.isArray(source) ? source : source?.value) as Array<{
|
|
912
1119
|
title?: string;
|
|
913
1120
|
value?: string;
|
|
914
1121
|
}>;
|
|
@@ -924,13 +1131,9 @@ function statusDisplay(raw: string, source: TableRow["source"]) {
|
|
|
924
1131
|
return raw.charAt(0).toUpperCase() + raw.slice(1).toLowerCase();
|
|
925
1132
|
}
|
|
926
1133
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (s === "pending") return "warning";
|
|
931
|
-
if (s === "in-active" || s === "inactive") return "default";
|
|
932
|
-
return "primary";
|
|
933
|
-
}
|
|
1134
|
+
/* The status chips read their tone from the shared mapping in
|
|
1135
|
+
`utils/status.ts` now - which carries every raw invitation status this map
|
|
1136
|
+
carried, with the same tone. */
|
|
934
1137
|
|
|
935
1138
|
// async function loadInviteRoles() {
|
|
936
1139
|
// loadingInviteRoles.value = true;
|
|
@@ -1006,7 +1209,12 @@ function mapInviteRows(raw: Record<string, any>[]): TableRow[] {
|
|
|
1006
1209
|
? typeDisplay(row.metadata.app)
|
|
1007
1210
|
: inviteTypeLabel(row.type ?? ""),
|
|
1008
1211
|
members: "-",
|
|
1009
|
-
|
|
1212
|
+
// Every invitation used to read "Pending" whatever had happened to it. It
|
|
1213
|
+
// now says which of the seven states it is actually in, in words.
|
|
1214
|
+
status: row.statusLabel || serviceProviderInviteLabel(row.status),
|
|
1215
|
+
statusRaw: String(row.status ?? ""),
|
|
1216
|
+
rejectionReason: row.rejectionReason ?? "",
|
|
1217
|
+
siteName: row.metadata?.siteName ?? "",
|
|
1010
1218
|
source: "invite",
|
|
1011
1219
|
}));
|
|
1012
1220
|
}
|
|
@@ -1022,13 +1230,14 @@ async function loadList() {
|
|
|
1022
1230
|
loading.value = true;
|
|
1023
1231
|
try {
|
|
1024
1232
|
if (listTab.value === "pending") {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1233
|
+
// Everything that is not a finished engagement, so a rejection or a
|
|
1234
|
+
// cancellation is visible here instead of the row simply vanishing.
|
|
1235
|
+
const data = await getInvitations({
|
|
1236
|
+
org: props.orgId,
|
|
1237
|
+
status:
|
|
1238
|
+
"awaiting-approval,pending,rejected,declined,cancelled,expired",
|
|
1028
1239
|
page: page.value,
|
|
1029
1240
|
search: searchText.value?.trim() || "",
|
|
1030
|
-
siteId: props.siteId,
|
|
1031
|
-
orgId: props.orgId,
|
|
1032
1241
|
limit: pageSize,
|
|
1033
1242
|
});
|
|
1034
1243
|
items.value = mapInviteRows(data.items ?? []);
|
|
@@ -1274,28 +1483,8 @@ async function submitServiceProviderInvite() {
|
|
|
1274
1483
|
</script>
|
|
1275
1484
|
|
|
1276
1485
|
<style scoped>
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
align-items: center;
|
|
1280
|
-
flex-wrap: nowrap;
|
|
1281
|
-
gap: 4px;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
.sp-table-header {
|
|
1285
|
-
background: #f5f5f5;
|
|
1286
|
-
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
.sp-tabs {
|
|
1290
|
-
width: 100%;
|
|
1291
|
-
min-height: 40px;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
.sp-tabs :deep(.v-tab) {
|
|
1295
|
-
min-width: 120px;
|
|
1296
|
-
justify-content: flex-start;
|
|
1297
|
-
text-transform: none;
|
|
1298
|
-
}
|
|
1486
|
+
/* The tab strip's own theming went with the strip: the tabs are the card's
|
|
1487
|
+
first row now and take `.app-tab` from the shared primitives. */
|
|
1299
1488
|
|
|
1300
1489
|
.sp-col-company {
|
|
1301
1490
|
max-width: 220px;
|