@7365admin1/layer-common 3.2.2-staging.92 → 3.2.2-staging.93
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/pm-sidebar-grouping-and-list-scaffold.md +27 -0
- package/assets/css/primitives.css +14 -0
- package/components/DashboardMain.vue +2 -2
- package/components/FeedbackMain.vue +33 -36
- package/components/InvitationMain.vue +45 -62
- package/components/Layout/NavigationDrawer.vue +4 -3
- package/components/ServiceProviderMain.vue +105 -135
- package/components/SwitchContext.vue +15 -1
- package/components/WorkOrder/Main.vue +42 -51
- package/package.json +1 -1
- package/utils/nav-sections.test.ts +70 -11
- package/utils/nav-sections.ts +48 -25
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/layer-common": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Group the sidebar's sections and give the list screens the design's page
|
|
6
|
+
scaffold.
|
|
7
|
+
|
|
8
|
+
- `withSections()` walked each application's menu in the order it was given and
|
|
9
|
+
printed a heading whenever the section changed, so Property Management drew
|
|
10
|
+
SERVICE DESK, PROPERTY and SECURITY OPERATIONS **twice each**. Items are now
|
|
11
|
+
grouped under one heading per section, in the order the design lists them,
|
|
12
|
+
with the application's own order kept inside each section. No menu item is
|
|
13
|
+
added, hidden, renamed, re-routed or re-gated.
|
|
14
|
+
- `ServiceProviderMain`, `FeedbackMain`, `InvitationMain` and `WorkOrder/Main`
|
|
15
|
+
had no page title, put their primary action top-left, floated the search
|
|
16
|
+
outside the card, and stacked the refresh/pager on a bar above the tabs. They
|
|
17
|
+
now use `PageHeader` / `AppCard` / `CardToolbar` / `AppButton` / `AppField`:
|
|
18
|
+
a 22px title with the action on the right, then one in-card row with the tabs
|
|
19
|
+
on the left and search, count and pager on the right.
|
|
20
|
+
- `SwitchContext` and the rail's application name print in full instead of
|
|
21
|
+
truncating to "Seventh Cond…" / "Property Managem…".
|
|
22
|
+
- The security dashboard's `ACTIVE PATROL` / `STAFF STATUS` panel headings are
|
|
23
|
+
written the way the design writes them, matching their sibling panels.
|
|
24
|
+
|
|
25
|
+
Placement and wording only: the same buttons open the same dialogs under the
|
|
26
|
+
same permissions, the same tabs load the same lists, and the tables, columns,
|
|
27
|
+
filters and row menus are untouched.
|
|
@@ -108,6 +108,9 @@
|
|
|
108
108
|
.app-toolbar {
|
|
109
109
|
display: flex;
|
|
110
110
|
align-items: center;
|
|
111
|
+
/* One row at desktop widths, as drawn. On a phone the tabs and the
|
|
112
|
+
search/pager group drop onto their own lines rather than overflow. */
|
|
113
|
+
flex-wrap: wrap;
|
|
111
114
|
gap: 22px;
|
|
112
115
|
padding: 0 var(--cellpad-x);
|
|
113
116
|
border-bottom: 1px solid var(--border);
|
|
@@ -129,6 +132,14 @@
|
|
|
129
132
|
gap: 8px;
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
/* The list toolbar's own search box. `--search` alone stretches (it is written
|
|
136
|
+
for the filter row), which would push the pager off the end of the row. */
|
|
137
|
+
.app-toolbar__search.app-field--search {
|
|
138
|
+
flex: 0 0 auto;
|
|
139
|
+
width: 200px;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
.app-toolbar__count {
|
|
133
144
|
font-size: 12.5px;
|
|
134
145
|
font-weight: 600;
|
|
@@ -161,6 +172,9 @@
|
|
|
161
172
|
color: var(--muted);
|
|
162
173
|
white-space: nowrap;
|
|
163
174
|
cursor: pointer;
|
|
175
|
+
/* A tab is a <button> on the screens that switch a list in place and a link
|
|
176
|
+
on the ones that route (Invitations), so the underline has to be ours. */
|
|
177
|
+
text-decoration: none;
|
|
164
178
|
}
|
|
165
179
|
|
|
166
180
|
.app-tab:hover {
|
|
@@ -888,7 +888,7 @@
|
|
|
888
888
|
<v-col v-if="isWidgetVisible('activePatrol')" cols="12" lg="6">
|
|
889
889
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
890
890
|
<div class="figma-panel-header">
|
|
891
|
-
<h2>
|
|
891
|
+
<h2>Active Patrol</h2>
|
|
892
892
|
<button
|
|
893
893
|
class="figma-panel-action"
|
|
894
894
|
type="button"
|
|
@@ -968,7 +968,7 @@
|
|
|
968
968
|
<v-col v-if="isWidgetVisible('staffStatus')" cols="12" lg="6">
|
|
969
969
|
<div class="dashboard-card dashboard-panel figma-panel">
|
|
970
970
|
<div class="figma-panel-header">
|
|
971
|
-
<h2>
|
|
971
|
+
<h2>Staff Status</h2>
|
|
972
972
|
<button
|
|
973
973
|
class="figma-panel-action"
|
|
974
974
|
type="button"
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<div>
|
|
3
|
+
<!-- The design's title row: the page's name at 22px and its primary
|
|
4
|
+
action on the right of that same line. Placement only - the same
|
|
5
|
+
button opens the same dialog under the same permission. -->
|
|
6
|
+
<PageHeader title="Feedbacks">
|
|
7
|
+
<template v-if="canCreateFeedback" #actions>
|
|
8
|
+
<AppButton @click="showCreateDialog = true">Create Feedback</AppButton>
|
|
9
|
+
</template>
|
|
10
|
+
</PageHeader>
|
|
11
|
+
|
|
12
|
+
<v-row no-gutters>
|
|
3
13
|
<v-col cols="12" class="mb-2">
|
|
4
14
|
<v-row no-gutters align="center" class="filter-bar">
|
|
5
15
|
<v-text-field
|
|
@@ -50,47 +60,32 @@
|
|
|
50
60
|
<v-date-picker v-model="endDate" width="320" :locale="locale" />
|
|
51
61
|
</v-card>
|
|
52
62
|
</v-dialog>
|
|
53
|
-
<v-btn
|
|
54
|
-
v-if="canCreateFeedback"
|
|
55
|
-
text="create feedback"
|
|
56
|
-
class="text-none text-capitalize ml-2 screen-btn-primary filter-bar__fit"
|
|
57
|
-
variant="flat"
|
|
58
|
-
@click="showCreateDialog = true"
|
|
59
|
-
/>
|
|
60
63
|
</v-row>
|
|
61
64
|
</v-col>
|
|
62
65
|
|
|
63
66
|
<v-col cols="12">
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
density="comfortable"
|
|
67
|
+
<AppCard class="table-card">
|
|
68
|
+
<!-- One row inside the card - refresh, the range and the pager - in
|
|
69
|
+
place of a `v-toolbar` that stacked them above the table. Same
|
|
70
|
+
three controls, same handlers. -->
|
|
71
|
+
<CardToolbar :count="pageRange">
|
|
72
|
+
<template #left>
|
|
73
|
+
<AppButton
|
|
74
|
+
variant="icon"
|
|
75
|
+
icon="mdi-refresh"
|
|
76
|
+
aria-label="Refresh"
|
|
75
77
|
@click="(page = 1), refreshFeedbacks()"
|
|
76
|
-
|
|
77
|
-
<v-icon icon="mdi-refresh" />
|
|
78
|
-
</v-btn>
|
|
78
|
+
/>
|
|
79
79
|
</template>
|
|
80
80
|
|
|
81
|
-
<template #
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
v-model="page"
|
|
88
|
-
:length="pages"
|
|
89
|
-
@update:value="updatePage"
|
|
90
|
-
/>
|
|
91
|
-
</v-row>
|
|
81
|
+
<template #right>
|
|
82
|
+
<local-pagination
|
|
83
|
+
v-model="page"
|
|
84
|
+
:length="pages"
|
|
85
|
+
@update:value="updatePage"
|
|
86
|
+
/>
|
|
92
87
|
</template>
|
|
93
|
-
</
|
|
88
|
+
</CardToolbar>
|
|
94
89
|
|
|
95
90
|
<v-data-table
|
|
96
91
|
:headers="headers"
|
|
@@ -100,6 +95,7 @@
|
|
|
100
95
|
fixed-header
|
|
101
96
|
hide-default-footer
|
|
102
97
|
:hide-default-header="false"
|
|
98
|
+
:loading="loading"
|
|
103
99
|
@click:row="tableRowClickHandler"
|
|
104
100
|
style="max-height: calc(100vh - (200px))"
|
|
105
101
|
>
|
|
@@ -173,9 +169,10 @@
|
|
|
173
169
|
<span v-else class="mt-1 text-wrap"> Not Assigned </span>
|
|
174
170
|
</template>
|
|
175
171
|
</v-data-table>
|
|
176
|
-
</
|
|
172
|
+
</AppCard>
|
|
177
173
|
</v-col>
|
|
178
|
-
|
|
174
|
+
</v-row>
|
|
175
|
+
</div>
|
|
179
176
|
|
|
180
177
|
<!-- <FeedbackForm
|
|
181
178
|
v-model="showCreateDialog"
|
|
@@ -1,69 +1,45 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
</v-col>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- The design's page scaffold: a title with the action on the right of
|
|
4
|
+
that line, then ONE row inside the card - the two tabs on the left,
|
|
5
|
+
refresh, the range and the pager on the right. The tabs route exactly
|
|
6
|
+
as they did; only where they sit has changed. -->
|
|
7
|
+
<PageHeader title="Invitations">
|
|
8
|
+
<template v-if="props.inviteMember" #actions>
|
|
9
|
+
<AppButton @click="inviteMember()">Invite member</AppButton>
|
|
10
|
+
</template>
|
|
11
|
+
</PageHeader>
|
|
12
|
+
|
|
13
|
+
<v-row no-gutters>
|
|
15
14
|
<v-col cols="12">
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
icon
|
|
26
|
-
density="comfortable"
|
|
27
|
-
@click="getVerifications()"
|
|
15
|
+
<AppCard class="table-card">
|
|
16
|
+
<CardToolbar :count="pageRange">
|
|
17
|
+
<template #tabs>
|
|
18
|
+
<NuxtLink
|
|
19
|
+
v-for="tab in invitationTabs"
|
|
20
|
+
:key="tab.status"
|
|
21
|
+
class="app-tab"
|
|
22
|
+
:class="{ 'app-tab--active': props.status === tab.status }"
|
|
23
|
+
:to="{ name: props.route, params: { status: tab.status } }"
|
|
28
24
|
>
|
|
29
|
-
|
|
30
|
-
</
|
|
31
|
-
</template>
|
|
32
|
-
|
|
33
|
-
<template #append>
|
|
34
|
-
<v-row no-gutters justify="end" align="center">
|
|
35
|
-
<span class="mr-2 table-card__range">
|
|
36
|
-
{{ pageRange }}
|
|
37
|
-
</span>
|
|
38
|
-
<local-pagination
|
|
39
|
-
v-model="page"
|
|
40
|
-
:length="pages"
|
|
41
|
-
@update:value="getVerifications"
|
|
42
|
-
/>
|
|
43
|
-
</v-row>
|
|
25
|
+
{{ tab.label }}
|
|
26
|
+
</NuxtLink>
|
|
44
27
|
</template>
|
|
45
28
|
|
|
46
|
-
<template #
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
name: props.route,
|
|
59
|
-
params: { status: 'expired' },
|
|
60
|
-
}"
|
|
61
|
-
>
|
|
62
|
-
Expired
|
|
63
|
-
</v-tab>
|
|
64
|
-
</v-tabs>
|
|
29
|
+
<template #right>
|
|
30
|
+
<AppButton
|
|
31
|
+
variant="icon"
|
|
32
|
+
icon="mdi-refresh"
|
|
33
|
+
aria-label="Refresh"
|
|
34
|
+
@click="getVerifications()"
|
|
35
|
+
/>
|
|
36
|
+
<local-pagination
|
|
37
|
+
v-model="page"
|
|
38
|
+
:length="pages"
|
|
39
|
+
@update:value="getVerifications"
|
|
40
|
+
/>
|
|
65
41
|
</template>
|
|
66
|
-
</
|
|
42
|
+
</CardToolbar>
|
|
67
43
|
|
|
68
44
|
<v-data-table
|
|
69
45
|
:headers="headers"
|
|
@@ -105,7 +81,7 @@
|
|
|
105
81
|
{{ formatStatus(value) }}
|
|
106
82
|
</template>
|
|
107
83
|
</v-data-table>
|
|
108
|
-
</
|
|
84
|
+
</AppCard>
|
|
109
85
|
</v-col>
|
|
110
86
|
|
|
111
87
|
<ConfirmDialog
|
|
@@ -155,10 +131,17 @@
|
|
|
155
131
|
:app="props.app"
|
|
156
132
|
/>
|
|
157
133
|
</v-dialog>
|
|
158
|
-
|
|
134
|
+
</v-row>
|
|
135
|
+
</div>
|
|
159
136
|
</template>
|
|
160
137
|
|
|
161
138
|
<script setup lang="ts">
|
|
139
|
+
/** The same two tabs, same labels, same routes. */
|
|
140
|
+
const invitationTabs = [
|
|
141
|
+
{ status: "pending", label: "Pending" },
|
|
142
|
+
{ status: "expired", label: "Expired" },
|
|
143
|
+
];
|
|
144
|
+
|
|
162
145
|
const props = defineProps({
|
|
163
146
|
app: {
|
|
164
147
|
type: String,
|
|
@@ -283,9 +283,10 @@ const handleClick = (item: any) => {
|
|
|
283
283
|
font-weight: var(--fw-card-title);
|
|
284
284
|
line-height: 1.2;
|
|
285
285
|
color: var(--text);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
/* The design prints the application's name in full ("Property Management").
|
|
287
|
+
Truncating it to "Property Managem…" in a 252px rail loses the word that
|
|
288
|
+
tells the user which product they are in, so it wraps instead. */
|
|
289
|
+
overflow-wrap: anywhere;
|
|
289
290
|
text-transform: capitalize;
|
|
290
291
|
}
|
|
291
292
|
|
|
@@ -1,108 +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
|
-
<div class="sp-table-header">
|
|
61
|
-
<!-- No fixed colour: `grey-lighten-4` is a light-mode grey in both
|
|
62
|
-
themes, which is what made this strip a white slab in dark
|
|
63
|
-
mode. `transparent` lets the themed .sp-table-header show. -->
|
|
64
|
-
<v-toolbar
|
|
65
|
-
density="compact"
|
|
66
|
-
color="transparent"
|
|
67
|
-
flat
|
|
68
|
-
class="sp-toolbar-top px-2"
|
|
69
|
-
>
|
|
70
|
-
<v-btn
|
|
71
|
-
icon
|
|
72
|
-
variant="text"
|
|
73
|
-
density="comfortable"
|
|
74
|
-
@click="loadList()"
|
|
75
|
-
>
|
|
76
|
-
<v-icon>mdi-refresh</v-icon>
|
|
77
|
-
</v-btn>
|
|
78
|
-
|
|
79
|
-
<v-spacer />
|
|
80
|
-
|
|
81
|
-
<template v-if="canViewServiceProviders">
|
|
82
|
-
<span class="mr-2 text-caption text-medium-emphasis">{{
|
|
83
|
-
pageRange
|
|
84
|
-
}}</span>
|
|
85
|
-
<local-pagination
|
|
86
|
-
v-model="page"
|
|
87
|
-
:length="pages"
|
|
88
|
-
@update:value="loadList()"
|
|
89
|
-
/>
|
|
90
|
-
</template>
|
|
91
|
-
</v-toolbar>
|
|
92
|
-
|
|
93
|
-
<v-tabs
|
|
94
|
-
v-model="listTab"
|
|
95
|
-
color="primary"
|
|
96
|
-
density="comfortable"
|
|
97
|
-
class="sp-tabs screen-tabs"
|
|
98
|
-
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"
|
|
99
60
|
>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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">
|
|
106
89
|
<v-data-table
|
|
107
90
|
v-if="canViewServiceProviders"
|
|
108
91
|
:headers="headers"
|
|
@@ -111,6 +94,7 @@
|
|
|
111
94
|
:items-per-page="pageSize"
|
|
112
95
|
fixed-header
|
|
113
96
|
hide-default-footer
|
|
97
|
+
:loading="loading"
|
|
114
98
|
class="sp-table"
|
|
115
99
|
style="max-height: calc(100vh - 280px)"
|
|
116
100
|
>
|
|
@@ -222,12 +206,16 @@
|
|
|
222
206
|
</v-menu>
|
|
223
207
|
</template>
|
|
224
208
|
</v-data-table>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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>
|
|
231
219
|
|
|
232
220
|
<v-dialog v-model="viewDialog" max-width="514">
|
|
233
221
|
<v-card rounded="lg" class="sp-members-card">
|
|
@@ -791,6 +779,12 @@ const { requiredRule, emailRule, debounce } = useUtils();
|
|
|
791
779
|
const { getUserByEmail } = useUser();
|
|
792
780
|
|
|
793
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
|
+
];
|
|
794
788
|
const page = ref(1);
|
|
795
789
|
const pages = ref(0);
|
|
796
790
|
const pageRange = ref("-- - -- of --");
|
|
@@ -1489,32 +1483,8 @@ async function submitServiceProviderInvite() {
|
|
|
1489
1483
|
</script>
|
|
1490
1484
|
|
|
1491
1485
|
<style scoped>
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
align-items: center;
|
|
1495
|
-
flex-wrap: nowrap;
|
|
1496
|
-
gap: 4px;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
.sp-table-header {
|
|
1500
|
-
/* Was a hardcoded #f5f5f5 with a black border: in dark mode the tab strip
|
|
1501
|
-
became a white slab and the tab labels and the page range disappeared
|
|
1502
|
-
into it. These take the colour of whichever theme they are in. */
|
|
1503
|
-
background: rgb(var(--v-theme-surface-light, var(--v-theme-surface)));
|
|
1504
|
-
border-bottom: 1px solid
|
|
1505
|
-
rgba(var(--v-border-color), var(--v-border-opacity));
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
.sp-tabs {
|
|
1509
|
-
width: 100%;
|
|
1510
|
-
min-height: 40px;
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
.sp-tabs :deep(.v-tab) {
|
|
1514
|
-
min-width: 120px;
|
|
1515
|
-
justify-content: flex-start;
|
|
1516
|
-
text-transform: none;
|
|
1517
|
-
}
|
|
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. */
|
|
1518
1488
|
|
|
1519
1489
|
.sp-col-company {
|
|
1520
1490
|
max-width: 220px;
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
:to="props.route"
|
|
16
16
|
class="text-subtitle-2 font-weight-bold"
|
|
17
17
|
>
|
|
18
|
-
|
|
18
|
+
<!-- The design prints the organisation and site names in full.
|
|
19
|
+
Vuetify's list-item title truncates by default, which cut them
|
|
20
|
+
to "Property Managem…" / "Seventh Cond…" in the 252px rail. -->
|
|
21
|
+
<v-list-item-title class="switch-context__name">
|
|
19
22
|
{{ name || `Select ${prop.title}` }}
|
|
20
23
|
</v-list-item-title>
|
|
21
24
|
</v-list-item>
|
|
@@ -124,3 +127,14 @@ const selectItem = (value: string) => {
|
|
|
124
127
|
emit("select", value);
|
|
125
128
|
};
|
|
126
129
|
</script>
|
|
130
|
+
|
|
131
|
+
<style scoped>
|
|
132
|
+
/* Wrap onto a second line instead of truncating - the design shows the full
|
|
133
|
+
organisation and site names. */
|
|
134
|
+
.switch-context__name {
|
|
135
|
+
white-space: normal;
|
|
136
|
+
overflow: visible;
|
|
137
|
+
overflow-wrap: anywhere;
|
|
138
|
+
line-height: 1.25;
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
@@ -1,58 +1,47 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
>
|
|
2
|
+
<div>
|
|
3
|
+
<!--
|
|
4
|
+
The design's page scaffold. The screen had no heading, its Create button
|
|
5
|
+
sat top-left and the search floated top-right outside the card, with
|
|
6
|
+
refresh and the pager stacked on their own bar above the table. Same
|
|
7
|
+
button, same dialog, same permission, same debounced search, same pager.
|
|
8
|
+
-->
|
|
9
|
+
<PageHeader title="Work Orders">
|
|
10
|
+
<template v-if="canCreateWorkOrder" #actions>
|
|
11
|
+
<AppButton @click="(showCreateDialog = true), (isEditMode = false)">
|
|
13
12
|
Create Work Order
|
|
14
|
-
</
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
variant="outlined"
|
|
20
|
-
density="comfortable"
|
|
21
|
-
clearable
|
|
22
|
-
hide-details
|
|
23
|
-
class="ml-2"
|
|
24
|
-
style="max-width: 250px"
|
|
25
|
-
/>
|
|
26
|
-
</v-row>
|
|
27
|
-
</v-col>
|
|
13
|
+
</AppButton>
|
|
14
|
+
</template>
|
|
15
|
+
</PageHeader>
|
|
16
|
+
|
|
17
|
+
<v-row no-gutters>
|
|
28
18
|
<v-col cols="12">
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<v-btn fab icon density="comfortable" @click="updatePage(1)">
|
|
39
|
-
<v-icon>mdi-refresh</v-icon>
|
|
40
|
-
</v-btn>
|
|
19
|
+
<AppCard class="table-card">
|
|
20
|
+
<CardToolbar :count="pageRange">
|
|
21
|
+
<template #left>
|
|
22
|
+
<AppButton
|
|
23
|
+
variant="icon"
|
|
24
|
+
icon="mdi-refresh"
|
|
25
|
+
aria-label="Refresh"
|
|
26
|
+
@click="updatePage(1)"
|
|
27
|
+
/>
|
|
41
28
|
</template>
|
|
42
29
|
|
|
43
|
-
<template #
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
<template #right>
|
|
31
|
+
<AppField
|
|
32
|
+
v-model="searchText"
|
|
33
|
+
search
|
|
34
|
+
compact
|
|
35
|
+
placeholder="Search work order..."
|
|
36
|
+
klass="app-toolbar__search"
|
|
37
|
+
/>
|
|
38
|
+
<local-pagination
|
|
39
|
+
v-model="page"
|
|
40
|
+
:length="pages"
|
|
41
|
+
@update:value="updatePage"
|
|
42
|
+
/>
|
|
54
43
|
</template>
|
|
55
|
-
</
|
|
44
|
+
</CardToolbar>
|
|
56
45
|
|
|
57
46
|
<v-data-table
|
|
58
47
|
:headers="headers"
|
|
@@ -62,6 +51,7 @@
|
|
|
62
51
|
fixed-header
|
|
63
52
|
hide-default-footer
|
|
64
53
|
:hide-default-header="false"
|
|
54
|
+
:loading="loading"
|
|
65
55
|
@click:row="tableRowClickHandler"
|
|
66
56
|
style="max-height: calc(100vh - (200px))"
|
|
67
57
|
>
|
|
@@ -69,9 +59,10 @@
|
|
|
69
59
|
{{ formatDateDDMMYYYYLocal(item.createdAt) }}
|
|
70
60
|
</template>
|
|
71
61
|
</v-data-table>
|
|
72
|
-
</
|
|
62
|
+
</AppCard>
|
|
73
63
|
</v-col>
|
|
74
|
-
|
|
64
|
+
</v-row>
|
|
65
|
+
</div>
|
|
75
66
|
|
|
76
67
|
<WorkOrderCreate
|
|
77
68
|
v-model="showCreateDialog"
|
package/package.json
CHANGED
|
@@ -28,16 +28,17 @@ const SECURITY = [
|
|
|
28
28
|
].map((title) => ({ title }));
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* THE RULE THAT MATTERS.
|
|
32
|
-
*
|
|
31
|
+
* THE RULE THAT MATTERS. Grouping may reorder the rail; it may never change
|
|
32
|
+
* the menu. Same objects, same count - nothing added, hidden or duplicated.
|
|
33
33
|
*/
|
|
34
|
-
test("
|
|
34
|
+
test("every menu item comes back exactly once, and nothing else does", () => {
|
|
35
35
|
const out = withSections(SECURITY);
|
|
36
36
|
|
|
37
37
|
assert.equal(out.length, SECURITY.length);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
assert.deepEqual(
|
|
39
|
+
new Set(out.map((e) => e.item)),
|
|
40
|
+
new Set(SECURITY)
|
|
41
|
+
);
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
test("Security is labelled the way the design draws it", () => {
|
|
@@ -102,10 +103,10 @@ test("an unmapped FIRST item prints no heading rather than a blank one", () => {
|
|
|
102
103
|
|
|
103
104
|
/**
|
|
104
105
|
* The applications do not agree on order: Landscape lists Feedbacks after its
|
|
105
|
-
* equipment modules.
|
|
106
|
-
*
|
|
106
|
+
* equipment modules. Those two Feedbacks/Work Orders items are collected under
|
|
107
|
+
* ONE SERVICE DESK heading instead of printing the heading twice.
|
|
107
108
|
*/
|
|
108
|
-
test("a section that comes back later is
|
|
109
|
+
test("a section that comes back later is grouped, not labelled twice", () => {
|
|
109
110
|
const out = withSections([
|
|
110
111
|
{ title: "Feedbacks" },
|
|
111
112
|
{ title: "Equipment Items" },
|
|
@@ -113,8 +114,66 @@ test("a section that comes back later is labelled again, not orphaned", () => {
|
|
|
113
114
|
]);
|
|
114
115
|
|
|
115
116
|
assert.deepEqual(
|
|
116
|
-
out.map((e) => e.section),
|
|
117
|
-
[
|
|
117
|
+
out.map((e) => `${e.section}|${e.item.title}`),
|
|
118
|
+
[
|
|
119
|
+
"SERVICE DESK|Feedbacks",
|
|
120
|
+
"|Work Orders",
|
|
121
|
+
"EQUIPMENT|Equipment Items",
|
|
122
|
+
]
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Property Management is the screen that showed the defect: its own menu order
|
|
128
|
+
* printed SERVICE DESK, PROPERTY and SECURITY OPERATIONS twice each.
|
|
129
|
+
*/
|
|
130
|
+
test("Property Management prints each heading exactly once", () => {
|
|
131
|
+
const PM = [
|
|
132
|
+
"Dashboard",
|
|
133
|
+
"Service Providers",
|
|
134
|
+
"Feedbacks",
|
|
135
|
+
"Work Orders",
|
|
136
|
+
"Facility Mgmt",
|
|
137
|
+
"Pass & Key Mgmt",
|
|
138
|
+
"HID Face Reader",
|
|
139
|
+
"Online Forms",
|
|
140
|
+
"NFC Patrol",
|
|
141
|
+
"Bulletin Board",
|
|
142
|
+
"Bulletin Videos",
|
|
143
|
+
"Visitor Mgmt",
|
|
144
|
+
"Document Mgmt",
|
|
145
|
+
"Access Mgmt",
|
|
146
|
+
].map((title) => ({ title }));
|
|
147
|
+
|
|
148
|
+
const out = withSections(PM);
|
|
149
|
+
const headings = out.map((e) => e.section).filter(Boolean);
|
|
150
|
+
|
|
151
|
+
assert.deepEqual(headings, [
|
|
152
|
+
"OVERVIEW",
|
|
153
|
+
"SERVICE DESK",
|
|
154
|
+
"PROPERTY",
|
|
155
|
+
"SECURITY OPERATIONS",
|
|
156
|
+
"COMMUNITY",
|
|
157
|
+
]);
|
|
158
|
+
assert.equal(new Set(headings).size, headings.length);
|
|
159
|
+
assert.deepEqual(
|
|
160
|
+
out.map((e) => e.item.title),
|
|
161
|
+
[
|
|
162
|
+
"Dashboard",
|
|
163
|
+
"Service Providers",
|
|
164
|
+
"Feedbacks",
|
|
165
|
+
"Work Orders",
|
|
166
|
+
"Online Forms",
|
|
167
|
+
"Facility Mgmt",
|
|
168
|
+
"Pass & Key Mgmt",
|
|
169
|
+
"Visitor Mgmt",
|
|
170
|
+
"Document Mgmt",
|
|
171
|
+
"Access Mgmt",
|
|
172
|
+
"HID Face Reader",
|
|
173
|
+
"NFC Patrol",
|
|
174
|
+
"Bulletin Board",
|
|
175
|
+
"Bulletin Videos",
|
|
176
|
+
]
|
|
118
177
|
);
|
|
119
178
|
});
|
|
120
179
|
|
package/utils/nav-sections.ts
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* THE GROUPED SIDEBAR - section
|
|
2
|
+
* THE GROUPED SIDEBAR - one heading per section, each printed once.
|
|
3
3
|
*
|
|
4
4
|
* The design draws the sidebar in labelled sections (OVERVIEW, SERVICE DESK,
|
|
5
5
|
* PROPERTY, SECURITY OPERATIONS, COMMUNITY, WORKFORCE, ADMINISTRATION). Each
|
|
6
6
|
* application builds its own menu array in its `layouts/default.vue`, gated
|
|
7
|
-
* item by item on permissions
|
|
7
|
+
* item by item on permissions.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* The applications do not agree on the order they list their modules in
|
|
10
|
+
* (Property Management interleaves five sections; Landscape puts Feedbacks
|
|
11
|
+
* after Equipment). Walking that order and printing a heading whenever the
|
|
12
|
+
* section changed - what this file used to do - made Property Management draw
|
|
13
|
+
* SERVICE DESK, PROPERTY and SECURITY OPERATIONS TWICE each. It reads as a
|
|
14
|
+
* bug, so the items are now GROUPED:
|
|
10
15
|
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* disappearing or forcing a stray
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* - every item is collected under its section, and each heading is printed
|
|
17
|
+
* exactly once, in the order the design lists the sections (which is the
|
|
18
|
+
* order they are declared in the map below).
|
|
19
|
+
* - inside a section the application's own order is preserved, so items that
|
|
20
|
+
* belong together stay in the sequence their author chose.
|
|
21
|
+
* - a title this map has never heard of inherits the section of the item
|
|
22
|
+
* above it, so a new menu item added by any of the eleven applications is
|
|
23
|
+
* grouped with its neighbours rather than disappearing or forcing a stray
|
|
24
|
+
* heading.
|
|
25
|
+
* - an item with no section at all (an unmapped FIRST item) leads the rail
|
|
26
|
+
* with no heading, exactly as before.
|
|
21
27
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* and printing the heading again is honest, where suppressing it would leave a
|
|
26
|
-
* run of items sitting under a heading that is not theirs.
|
|
28
|
+
* This changes what ORDER the items are drawn in and nothing else. No item is
|
|
29
|
+
* added, hidden, renamed, re-routed or re-gated - the array that comes out
|
|
30
|
+
* holds the same objects as the array that went in.
|
|
27
31
|
*/
|
|
28
32
|
|
|
29
33
|
export const NAV_SECTIONS: Record<string, string> = {
|
|
@@ -110,19 +114,38 @@ export const NAV_SECTIONS: Record<string, string> = {
|
|
|
110
114
|
|
|
111
115
|
export type TSectionedNavItem<T> = { item: T; section: string };
|
|
112
116
|
|
|
117
|
+
/** The design's section order - the order they are declared above. */
|
|
118
|
+
export const SECTION_ORDER: string[] = [
|
|
119
|
+
...new Set(Object.values(NAV_SECTIONS)),
|
|
120
|
+
];
|
|
121
|
+
|
|
113
122
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
123
|
+
* Groups the menu by section and tags each item with the heading to print
|
|
124
|
+
* above it, or "" when the heading is already printed. Same length, same
|
|
125
|
+
* objects; only the order changes.
|
|
116
126
|
*/
|
|
117
127
|
export function withSections<T extends { title?: string }>(
|
|
118
128
|
items: T[]
|
|
119
129
|
): Array<TSectionedNavItem<T>> {
|
|
130
|
+
// An unmapped title inherits the section of the item above it.
|
|
120
131
|
let current = "";
|
|
132
|
+
const resolved = items.map((item, index) => {
|
|
133
|
+
current = NAV_SECTIONS[String(item?.title ?? "")] ?? current;
|
|
134
|
+
return { item, section: current, index };
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Sectionless items (an unmapped first item) rank -1 and lead the rail.
|
|
138
|
+
// Inside a section the application's own order is kept.
|
|
139
|
+
const grouped = [...resolved].sort(
|
|
140
|
+
(a, b) =>
|
|
141
|
+
SECTION_ORDER.indexOf(a.section) - SECTION_ORDER.indexOf(b.section) ||
|
|
142
|
+
a.index - b.index
|
|
143
|
+
);
|
|
121
144
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
return { item, section:
|
|
145
|
+
let printed = "";
|
|
146
|
+
return grouped.map(({ item, section }) => {
|
|
147
|
+
const label = section === printed ? "" : section;
|
|
148
|
+
printed = section;
|
|
149
|
+
return { item, section: label };
|
|
127
150
|
});
|
|
128
151
|
}
|