@7365admin1/layer-common 3.2.2-staging.91 → 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.
@@ -1,69 +1,45 @@
1
1
  <template>
2
- <v-row no-gutters>
3
- <v-col cols="12" class="mb-2">
4
- <v-row no-gutters>
5
- <v-btn
6
- class="text-none mr-2 screen-btn-primary"
7
- variant="flat"
8
- @click="inviteMember()"
9
- v-if="props.inviteMember"
10
- >
11
- Invite member
12
- </v-btn>
13
- </v-row>
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
- <v-card width="100%" variant="flat" class="table-card">
17
- <v-toolbar
18
- density="compact"
19
- color="transparent"
20
- class="table-card__toolbar"
21
- >
22
- <template #prepend>
23
- <v-btn
24
- fab
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
- <v-icon>mdi-refresh</v-icon>
30
- </v-btn>
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 #extension>
47
- <v-tabs>
48
- <v-tab
49
- :to="{
50
- name: props.route,
51
- params: { status: 'pending' },
52
- }"
53
- >
54
- Pending
55
- </v-tab>
56
- <v-tab
57
- :to="{
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
- </v-toolbar>
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
- </v-card>
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
- </v-row>
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,
@@ -126,7 +126,18 @@ const { currentUser } = useLocalAuth();
126
126
  */
127
127
  const { site } = useSite();
128
128
 
129
- const siteName = computed(() => String(site.value?.name ?? "").trim());
129
+ /**
130
+ * The shared `site` state is only populated by the screens that load a site
131
+ * document, so on most screens - the dashboard included - the bar had no name
132
+ * to print. `SwitchContext` mirrors the label it is ALREADY showing in the
133
+ * rail into this state, so the bar falls back to the very name the user can
134
+ * see two inches to the left. Still no request from this component.
135
+ */
136
+ const shellSiteName = useState<string>("shell-context-site-name", () => "");
137
+
138
+ const siteName = computed(() =>
139
+ String(site.value?.name || shellSiteName.value || "").trim()
140
+ );
130
141
 
131
142
  const route = useRoute();
132
143
 
@@ -188,8 +199,11 @@ const name = computed(() => {
188
199
 
189
200
  if(full) return full;
190
201
 
202
+ /* A member invited by email has no name stored yet, which left the avatar's
203
+ initials empty and the circle blank. The address is what the product shows
204
+ that member everywhere else, so it stands in here too. */
191
205
  const alternative = user?.name?.trim();
192
- return alternative || ""
206
+ return alternative || user?.email?.trim() || ""
193
207
 
194
208
  })
195
209
 
@@ -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
- overflow: hidden;
287
- white-space: nowrap;
288
- text-overflow: ellipsis;
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
- <v-row no-gutters>
3
- <v-col cols="12" class="mb-3">
4
- <v-row no-gutters align="center" justify="space-between">
5
- <div class="d-flex flex-wrap align-center">
6
- <v-btn
7
- v-if="showAddButton && canAddServiceProvider"
8
- class="text-none mr-2 screen-btn-primary"
9
- variant="flat"
10
- @click="(createDialog = true), setServiceProvider({ mode: 'add' })"
11
- >
12
- Add
13
- </v-btn>
14
- <v-btn
15
- v-if="canInviteServiceProvider"
16
- class="text-none mr-2 screen-btn-primary"
17
- variant="flat"
18
- @click="
19
- (createInviteDialog = true),
20
- setServiceProvider({ mode: 'invite' })
21
- "
22
- >
23
- Invite
24
- </v-btn>
25
- <v-btn
26
- v-if="showBillingButton"
27
- class="text-none mr-2 screen-btn-primary"
28
- variant="flat"
29
- @click="
30
- useRouter().push({
31
- name: 'org-site-service-provider-mgmt-billing',
32
- })
33
- "
34
- >
35
- Billing
36
- </v-btn>
37
- </div>
38
-
39
- <v-text-field
40
- v-model="searchText"
41
- placeholder="Search"
42
- variant="outlined"
43
- density="comfortable"
44
- clearable
45
- hide-details
46
- prepend-inner-icon="mdi-magnify"
47
- class="ml-2"
48
- style="max-width: 280px"
49
- />
50
- </v-row>
51
- </v-col>
52
-
53
- <v-col cols="12">
54
- <v-card
55
- width="100%"
56
- variant="flat"
57
- class="table-card"
58
- :loading="loading"
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
- <v-tab value="active" class="text-none">Service Providers</v-tab>
101
- <v-tab value="pending" class="text-none">Pending</v-tab>
102
- <v-tab value="inactive" class="text-none">In-Active</v-tab>
103
- </v-tabs>
104
- </div>
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
- <v-card-text v-else class="text-body-2 text-medium-emphasis">
226
- You do not have permission to view service providers.
227
- </v-card-text>
228
- </v-card>
229
- </v-col>
230
- </v-row>
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
- .sp-toolbar-top {
1493
- display: flex;
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
- <v-list-item-title class="text-truncate">
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>
@@ -100,9 +103,38 @@ const name = computed(() => {
100
103
  return prop.items.find((item) => item.value === selected.value)?.text ?? "";
101
104
  });
102
105
 
106
+ /**
107
+ * THE BREADCRUMB'S SITE HALF.
108
+ *
109
+ * The top bar draws `Site name / Page`, but the site's NAME is only ever
110
+ * resolved here - each application loads its own site list and hands it to
111
+ * this selector, so the bar had nothing to print and showed the page alone.
112
+ * The label already on screen is mirrored into shared state for the bar to
113
+ * read. It is a label: no request is made, and what the selector selects is
114
+ * untouched.
115
+ */
116
+ const shellSiteName = useState<string>("shell-context-site-name", () => "");
117
+
118
+ watchEffect(() => {
119
+ if (prop.title?.toLowerCase() === "site" && name.value) {
120
+ shellSiteName.value = name.value;
121
+ }
122
+ });
123
+
103
124
  const selectItem = (value: string) => {
104
125
  selected.value = value;
105
126
  menu.value = false;
106
127
  emit("select", value);
107
128
  };
108
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
- <v-row no-gutters>
3
- <v-col cols="12" class="mb-2">
4
- <v-row no-gutters align="center" justify="space-between">
5
- <v-btn
6
- class="text-none text-capitalize"
7
- rounded="pill"
8
- variant="tonal"
9
- size="large"
10
- @click="(showCreateDialog = true), (isEditMode = false)"
11
- v-if="canCreateWorkOrder"
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
- </v-btn>
15
-
16
- <v-text-field
17
- v-model="searchText"
18
- placeholder="Search work order..."
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
- <v-card
30
- width="100%"
31
- variant="outlined"
32
- border="thin"
33
- rounded="lg"
34
- :loading="loading"
35
- >
36
- <v-toolbar density="compact" color="grey-lighten-4">
37
- <template #prepend>
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 #append>
44
- <v-row no-gutters justify="end" align="center">
45
- <span class="mr-2 text-caption text-fontgray">
46
- {{ pageRange }}
47
- </span>
48
- <local-pagination
49
- v-model="page"
50
- :length="pages"
51
- @update:value="updatePage"
52
- />
53
- </v-row>
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
- </v-toolbar>
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
- </v-card>
62
+ </AppCard>
73
63
  </v-col>
74
- </v-row>
64
+ </v-row>
65
+ </div>
75
66
 
76
67
  <WorkOrderCreate
77
68
  v-model="showCreateDialog"
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.2.2-staging.91",
5
+ "version": "3.2.2-staging.93",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {