@7365admin1/layer-common 3.2.2-staging.192 → 3.2.2-staging.194
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/components/AccessManagement.vue +33 -13
- package/components/BuildingForm.vue +3 -1
- package/components/BuildingManagement/buildings.vue +13 -2
- package/components/BulletinBoardManagement.vue +26 -22
- package/components/DocumentManagement.vue +23 -5
- package/components/ManageChecklistMain.vue +23 -5
- package/components/TableHygiene.vue +9 -2
- package/components/TableListSecondary.vue +10 -0
- package/components/WorkOrder/ListView.vue +36 -4
- package/package.json +1 -1
- package/utils/data.test.ts +24 -1
- package/utils/data.ts +15 -0
- package/utils/list-page.test.ts +60 -0
- package/utils/list-page.ts +38 -0
- package/components/ScheduleTastTicketMain.vue +0 -157
|
@@ -62,6 +62,15 @@
|
|
|
62
62
|
klass="app-toolbar__search"
|
|
63
63
|
/>
|
|
64
64
|
</template>
|
|
65
|
+
|
|
66
|
+
<!--
|
|
67
|
+
A failed request is not a site with no access cards. Without this the
|
|
68
|
+
table drew its neutral "No data available" for a 500, which reads as
|
|
69
|
+
"there are no cards here" - a statement the server never made.
|
|
70
|
+
-->
|
|
71
|
+
<template v-if="getCardError" #no-data>
|
|
72
|
+
<DashboardEmptyState error />
|
|
73
|
+
</template>
|
|
65
74
|
</TableMain>
|
|
66
75
|
|
|
67
76
|
<!-- Create Dialog -->
|
|
@@ -125,6 +134,10 @@
|
|
|
125
134
|
</v-row>
|
|
126
135
|
</template>
|
|
127
136
|
<script setup lang="ts">
|
|
137
|
+
// Relative import, never Nuxt auto-import: this layer's `utils/` are auto-imported
|
|
138
|
+
// into the consuming apps but not reliably into the layer's own components.
|
|
139
|
+
import { readListPage } from "../utils/list-page";
|
|
140
|
+
|
|
128
141
|
definePageMeta({
|
|
129
142
|
middleware: ["01-auth", "02-org"],
|
|
130
143
|
memberOnly: true,
|
|
@@ -262,6 +275,7 @@ const {
|
|
|
262
275
|
data: getCardReq,
|
|
263
276
|
refresh: getCards,
|
|
264
277
|
status: getAllReqStatus,
|
|
278
|
+
error: getCardError,
|
|
265
279
|
} = useLazyAsyncData(
|
|
266
280
|
"get-all-cards",
|
|
267
281
|
() =>
|
|
@@ -288,21 +302,27 @@ watch(searchText, (val) => {
|
|
|
288
302
|
|
|
289
303
|
const loading = computed(() => getAllReqStatus.value === "pending");
|
|
290
304
|
|
|
305
|
+
// This endpoint answers with a NESTED envelope - `{ data: { items, pages,
|
|
306
|
+
// pageRange } }` - so the page totals are read off `reply.data`, through the
|
|
307
|
+
// one helper that knows how a paginated reply is shaped. The totals are the
|
|
308
|
+
// server's count of the WHOLE list; they are never re-derived from the rows on
|
|
309
|
+
// this page. Applying it unconditionally matters: when a later request fails,
|
|
310
|
+
// the reply goes null and the header falls back to the placeholder instead of
|
|
311
|
+
// keeping the previous page's total on screen as if it were still true.
|
|
291
312
|
watchEffect(() => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
},
|
|
313
|
+
const listPage = readListPage<Record<string, any>>(getCardReq.value?.data);
|
|
314
|
+
items.value = listPage.items.map((item: any) => ({
|
|
315
|
+
...item,
|
|
316
|
+
cardCounts: {
|
|
317
|
+
...item.cardCounts,
|
|
318
|
+
available: {
|
|
319
|
+
...item.cardCounts?.available,
|
|
320
|
+
physical: (item.available?.physical ?? []).filter((c: any) => !c.damage).length,
|
|
301
321
|
},
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
322
|
+
},
|
|
323
|
+
}));
|
|
324
|
+
pages.value = listPage.pages;
|
|
325
|
+
pageRange.value = listPage.pageRange;
|
|
306
326
|
});
|
|
307
327
|
|
|
308
328
|
function setCard({ mode = "create", dialog = true, data = {} as TCard } = {}) {
|
|
@@ -353,7 +353,9 @@ const buildingFilesNames = ref<Record<string, string>>({});
|
|
|
353
353
|
building.value.site = prop.site;
|
|
354
354
|
if (prop.mode === "edit") {
|
|
355
355
|
building.value = JSON.parse(JSON.stringify(prop.building));
|
|
356
|
-
|
|
356
|
+
// Same missing field as the blocks table: a block record without `levels`
|
|
357
|
+
// made this `.map` throw, which killed the edit dialog before it opened.
|
|
358
|
+
building.value.levels = (building.value.levels ?? []).map((level: any) => ({ _id: level._id, name: level.name })); //temporary
|
|
357
359
|
buildingLevels.value = building.value.levels.length;
|
|
358
360
|
initialBuildingLevels.value = JSON.parse(JSON.stringify(building.value.levels));
|
|
359
361
|
|
|
@@ -25,8 +25,13 @@
|
|
|
25
25
|
<template #item.createdAt="{ value }">
|
|
26
26
|
{{ new Date(value).toLocaleDateString() }}
|
|
27
27
|
</template>
|
|
28
|
+
<!--
|
|
29
|
+
`item.levels.length` threw during render for any block whose record has
|
|
30
|
+
no `levels` array, and a TypeError in a row template tears the whole
|
|
31
|
+
table down - one incomplete block blanked every block.
|
|
32
|
+
-->
|
|
28
33
|
<template #item.levels="{ item }">
|
|
29
|
-
{{ item
|
|
34
|
+
{{ levelCount(item) }}
|
|
30
35
|
</template>
|
|
31
36
|
</TableMain>
|
|
32
37
|
|
|
@@ -58,7 +63,9 @@
|
|
|
58
63
|
|
|
59
64
|
<v-col cols="12">
|
|
60
65
|
<strong>Levels:</strong>
|
|
61
|
-
|
|
66
|
+
<!-- The optional chain stopped one property early: `?.levels.length`
|
|
67
|
+
still reads `.length` of undefined when the block has no levels. -->
|
|
68
|
+
{{ selectedBuilding ? levelCount(selectedBuilding) : "N/A" }}
|
|
62
69
|
</v-col>
|
|
63
70
|
<v-col cols="12">
|
|
64
71
|
<strong>Files:</strong>
|
|
@@ -151,6 +158,10 @@
|
|
|
151
158
|
</template>
|
|
152
159
|
|
|
153
160
|
<script setup lang="ts">
|
|
161
|
+
// Relative import, never Nuxt auto-import: this layer's `utils/` are auto-imported
|
|
162
|
+
// into the consuming apps but not reliably into the layer's own components.
|
|
163
|
+
import { levelCount } from "../../utils/data";
|
|
164
|
+
|
|
154
165
|
definePageMeta({
|
|
155
166
|
middleware: ["01-auth", "02-org"],
|
|
156
167
|
memberOnly: true,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
</PageHeader>
|
|
12
12
|
</v-col>
|
|
13
13
|
|
|
14
|
-
<TableMain :headers="headers" :items="
|
|
14
|
+
<TableMain :headers="headers" :items="items" v-model:search="searchInput"
|
|
15
15
|
:loading="getAnnouncementPending" :page="page" :pages="pages" :pageRange="pageRange"
|
|
16
16
|
@refresh="getAnnouncementsRefresh" :show-header="APP_CONSTANTS.RESIDENT === props.recipients" @update:page="handleUpdatePage" @row-click="handleRowClick">
|
|
17
17
|
<!--
|
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
</button>
|
|
28
28
|
</template>
|
|
29
29
|
|
|
30
|
+
<!--
|
|
31
|
+
A failed request is not an empty bulletin board. Without this the
|
|
32
|
+
table drew its neutral "No data available" for a 500, which reads
|
|
33
|
+
as "there are no announcements" - a statement the server never made.
|
|
34
|
+
-->
|
|
35
|
+
<template v-if="getAnnouncementError" #no-data>
|
|
36
|
+
<DashboardEmptyState error />
|
|
37
|
+
</template>
|
|
38
|
+
|
|
30
39
|
<template v-slot:item.createdAt="{ item }">
|
|
31
40
|
<span class="d-flex align-center ga-2">
|
|
32
41
|
<v-icon icon="mdi-calendar-start" color="green" size="20" />
|
|
@@ -75,6 +84,10 @@
|
|
|
75
84
|
<script setup lang="ts">
|
|
76
85
|
import useBulletin from '../composables/useBulletin';
|
|
77
86
|
import { APP_CONSTANTS } from '../constants/app';
|
|
87
|
+
// Imported by relative path, not left to Nuxt's auto-import: this layer's
|
|
88
|
+
// components always import their utils explicitly, and a missing import here
|
|
89
|
+
// is a ReferenceError that blanks the screen in every consuming app.
|
|
90
|
+
import { readListPage } from '../utils/list-page';
|
|
78
91
|
|
|
79
92
|
definePageMeta({
|
|
80
93
|
memberOnly: true,
|
|
@@ -135,6 +148,13 @@ const headers = computed(() => {
|
|
|
135
148
|
return arr;
|
|
136
149
|
|
|
137
150
|
});
|
|
151
|
+
// THE SERVER PAGES THIS LIST. `getAll` is called with `page`, so `items` is
|
|
152
|
+
// already one page of rows and `pages` / `pageRange` are the server's own
|
|
153
|
+
// answer for the whole list. A computed used to re-derive both from
|
|
154
|
+
// `items.value.length` - i.e. from the page it had just been handed - so 47
|
|
155
|
+
// announcements read "1-10 of 10", the pager was told there was 1 page, and
|
|
156
|
+
// page 2 was unreachable. Nothing here recomputes them any more; the watcher
|
|
157
|
+
// below is the only writer.
|
|
138
158
|
const items = ref<TAnnouncement[]>([]);
|
|
139
159
|
const page = ref(1);
|
|
140
160
|
const pages = ref(0);
|
|
@@ -163,28 +183,11 @@ const activeAnnouncementObj = computed(() => {
|
|
|
163
183
|
return items.value.find(x => x._id === selectedAnnouncementId.value)
|
|
164
184
|
})
|
|
165
185
|
|
|
166
|
-
const paginatePlaceholderItem = computed(() => {
|
|
167
|
-
const pageSize = 10
|
|
168
|
-
const total = items.value.length
|
|
169
|
-
const currentPage = page.value
|
|
170
|
-
|
|
171
|
-
const start = (currentPage - 1) * pageSize
|
|
172
|
-
const end = start + pageSize
|
|
173
|
-
|
|
174
|
-
const from = total === 0 ? 0 : start + 1
|
|
175
|
-
const to = Math.min(end, total)
|
|
176
|
-
pageRange.value = `${from}-${to} of ${total}`
|
|
177
|
-
|
|
178
|
-
pages.value = Math.ceil(total / pageSize)
|
|
179
|
-
|
|
180
|
-
return items.value.slice(start, end)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
|
|
184
186
|
const {
|
|
185
187
|
data: getAnnouncementReq,
|
|
186
188
|
refresh: getAnnouncementsRefresh,
|
|
187
189
|
pending: getAnnouncementPending,
|
|
190
|
+
error: getAnnouncementError,
|
|
188
191
|
} = await useLazyAsyncData(
|
|
189
192
|
`get-all-announcements-${page.value}`,
|
|
190
193
|
() => getAll({ page: page.value, site: siteId, status: status.value, recipients: props.recipients ?? "" }),
|
|
@@ -194,9 +197,10 @@ const {
|
|
|
194
197
|
);
|
|
195
198
|
|
|
196
199
|
watch(getAnnouncementReq, (newVal) => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
const listPage = readListPage<TAnnouncement>(newVal);
|
|
201
|
+
items.value = listPage.items;
|
|
202
|
+
pages.value = listPage.pages;
|
|
203
|
+
pageRange.value = listPage.pageRange;
|
|
200
204
|
});
|
|
201
205
|
|
|
202
206
|
|
|
@@ -85,6 +85,15 @@
|
|
|
85
85
|
@update:page="page = $event"
|
|
86
86
|
@row-click="tableRowClickHandler"
|
|
87
87
|
>
|
|
88
|
+
<!--
|
|
89
|
+
A failed request is not an empty folder. Without this the table drew
|
|
90
|
+
its neutral "No data available" for a 500, which reads as "there are
|
|
91
|
+
no documents here" - a statement the server never made.
|
|
92
|
+
-->
|
|
93
|
+
<template v-if="getDocumentError" #no-data>
|
|
94
|
+
<DashboardEmptyState error />
|
|
95
|
+
</template>
|
|
96
|
+
|
|
88
97
|
<template v-if="isInsideFolder" #breadcrumb>
|
|
89
98
|
<div class="d-flex align-center flex-wrap ga-1">
|
|
90
99
|
<v-btn
|
|
@@ -597,6 +606,10 @@
|
|
|
597
606
|
</v-row>
|
|
598
607
|
</template>
|
|
599
608
|
<script setup lang="ts">
|
|
609
|
+
// Relative import, never Nuxt auto-import: this layer's `utils/` are auto-imported
|
|
610
|
+
// into the consuming apps but not reliably into the layer's own components.
|
|
611
|
+
import { readListPage } from "../utils/list-page";
|
|
612
|
+
|
|
600
613
|
definePageMeta({
|
|
601
614
|
middleware: ["01-auth", "02-org"],
|
|
602
615
|
memberOnly: true,
|
|
@@ -736,6 +749,7 @@ const {
|
|
|
736
749
|
data: getDocumentReq,
|
|
737
750
|
refresh: getDocuments,
|
|
738
751
|
status: getAllReqStatus,
|
|
752
|
+
error: getDocumentError,
|
|
739
753
|
} = useLazyAsyncData(
|
|
740
754
|
"get-all-documents",
|
|
741
755
|
() =>
|
|
@@ -754,12 +768,16 @@ const {
|
|
|
754
768
|
|
|
755
769
|
const loading = computed(() => getAllReqStatus.value === "pending");
|
|
756
770
|
|
|
771
|
+
// The page totals are the server's count of the WHOLE list, read through the
|
|
772
|
+
// one helper that knows how a paginated reply is shaped - never re-derived
|
|
773
|
+
// from the rows on this page. Applied unconditionally so that when a request
|
|
774
|
+
// fails the reply goes null and the header falls back to the placeholder,
|
|
775
|
+
// instead of leaving the previous page's total on screen as if it still held.
|
|
757
776
|
watchEffect(() => {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
777
|
+
const listPage = readListPage<Record<string, any>>(getDocumentReq.value);
|
|
778
|
+
items.value = listPage.items;
|
|
779
|
+
pages.value = listPage.pages;
|
|
780
|
+
pageRange.value = listPage.pageRange;
|
|
763
781
|
});
|
|
764
782
|
|
|
765
783
|
function formatDate(value?: string | null) {
|
|
@@ -29,6 +29,15 @@
|
|
|
29
29
|
:show-header="true"
|
|
30
30
|
@refresh="getUnitCleanerChecklistRefresh"
|
|
31
31
|
>
|
|
32
|
+
<!--
|
|
33
|
+
A failed request is not a schedule with no checklist. Without this the
|
|
34
|
+
table drew its neutral "No checklist found" for a 500, which reads as
|
|
35
|
+
"nothing is scheduled here" - a statement the server never made.
|
|
36
|
+
-->
|
|
37
|
+
<template v-if="getUnitCleanerChecklistError" #no-data>
|
|
38
|
+
<DashboardEmptyState error />
|
|
39
|
+
</template>
|
|
40
|
+
|
|
32
41
|
<!-- Status icon driven by local activeActions state -->
|
|
33
42
|
<template #item-prepend="{ item, group }">
|
|
34
43
|
<!--
|
|
@@ -441,6 +450,7 @@
|
|
|
441
450
|
</template>
|
|
442
451
|
|
|
443
452
|
<script setup lang="ts">
|
|
453
|
+
import { readListPage } from "../utils/list-page";
|
|
444
454
|
import useCleaningSchedules from "../composables/useCleaningSchedules";
|
|
445
455
|
import useCustomerSite from "../composables/useCustomerSite";
|
|
446
456
|
import useFile from "../composables/useFile";
|
|
@@ -651,6 +661,7 @@ const {
|
|
|
651
661
|
data: getUnitCleanerChecklistReq,
|
|
652
662
|
refresh: getUnitCleanerChecklistRefresh,
|
|
653
663
|
pending: loading,
|
|
664
|
+
error: getUnitCleanerChecklistError,
|
|
654
665
|
} = await useLazyAsyncData(
|
|
655
666
|
`get-all-unit-cleaner-checklist`,
|
|
656
667
|
() =>
|
|
@@ -675,13 +686,20 @@ watchEffect(() => {
|
|
|
675
686
|
}
|
|
676
687
|
});
|
|
677
688
|
|
|
689
|
+
// The page totals are the server's count of the WHOLE list, read through the
|
|
690
|
+
// one helper that knows how a paginated reply is shaped - never re-derived
|
|
691
|
+
// from the rows on this page. Applied unconditionally so that when a request
|
|
692
|
+
// fails the reply goes null and the header falls back to the placeholder,
|
|
693
|
+
// instead of leaving the previous page's total on screen as if it still held.
|
|
694
|
+
// `page` still only moves when the server states it, so a failed request does
|
|
695
|
+
// not knock the reader back to page 1.
|
|
678
696
|
watchEffect(() => {
|
|
679
|
-
|
|
680
|
-
|
|
697
|
+
const listPage = readListPage<TChecklistSet>(getUnitCleanerChecklistReq.value);
|
|
698
|
+
items.value = listPage.items;
|
|
699
|
+
if (getUnitCleanerChecklistReq.value?.page)
|
|
681
700
|
page.value = getUnitCleanerChecklistReq.value.page;
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
701
|
+
pages.value = listPage.pages;
|
|
702
|
+
pageRange.value = listPage.pageRange;
|
|
685
703
|
});
|
|
686
704
|
|
|
687
705
|
watchEffect(() => {
|
|
@@ -62,8 +62,15 @@
|
|
|
62
62
|
class="overflow-y-auto"
|
|
63
63
|
>
|
|
64
64
|
<div v-if="groupedItems.length === 0" class="table-card__empty">
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
<!--
|
|
66
|
+
The empty block was hard-coded, so a caller had no way to say
|
|
67
|
+
"this is not empty, the request failed" - every 500 rendered as
|
|
68
|
+
the neutral "nothing here". The default is unchanged.
|
|
69
|
+
-->
|
|
70
|
+
<slot name="no-data">
|
|
71
|
+
<v-icon icon="mdi-magnify-close" size="32" />
|
|
72
|
+
<span>{{ noDataText }}</span>
|
|
73
|
+
</slot>
|
|
67
74
|
</div>
|
|
68
75
|
|
|
69
76
|
<template
|
|
@@ -96,6 +96,16 @@
|
|
|
96
96
|
</td>
|
|
97
97
|
</tr>
|
|
98
98
|
</template>
|
|
99
|
+
|
|
100
|
+
<!--
|
|
101
|
+
Forwarded so a caller can say "this is not empty, the request
|
|
102
|
+
failed". Without it every 500 rendered as the table's neutral
|
|
103
|
+
"No data available". Vuetify's own default is unchanged when the
|
|
104
|
+
caller passes nothing.
|
|
105
|
+
-->
|
|
106
|
+
<template v-if="$slots['no-data']" #no-data>
|
|
107
|
+
<slot name="no-data" />
|
|
108
|
+
</template>
|
|
99
109
|
</v-data-table>
|
|
100
110
|
</AppCard>
|
|
101
111
|
</div>
|
|
@@ -13,6 +13,15 @@
|
|
|
13
13
|
@click:create="emit('click:create')"
|
|
14
14
|
:length="pages"
|
|
15
15
|
>
|
|
16
|
+
<!--
|
|
17
|
+
A failed request is not a site with no work orders. Without this the
|
|
18
|
+
table drew its neutral "No data available" for a 500, which reads as
|
|
19
|
+
"there is nothing outstanding" - a statement the server never made.
|
|
20
|
+
-->
|
|
21
|
+
<template v-if="failed" #no-data>
|
|
22
|
+
<DashboardEmptyState error />
|
|
23
|
+
</template>
|
|
24
|
+
|
|
16
25
|
<template #title>
|
|
17
26
|
<span class="screen-card-title">Work Order</span>
|
|
18
27
|
</template>
|
|
@@ -52,6 +61,8 @@
|
|
|
52
61
|
</template>
|
|
53
62
|
|
|
54
63
|
<script lang="ts" setup>
|
|
64
|
+
import { readListPage, NO_PAGE_RANGE } from "../../utils/list-page";
|
|
65
|
+
|
|
55
66
|
const emit = defineEmits(["click:create", "update:pagination"]);
|
|
56
67
|
|
|
57
68
|
const { page, pages, pageRange, workOrders, getWorkOrders } = useWorkOrder();
|
|
@@ -61,6 +72,7 @@ const message = ref("");
|
|
|
61
72
|
const messageColor = ref("");
|
|
62
73
|
const messageSnackbar = ref(false);
|
|
63
74
|
const loading = ref(false);
|
|
75
|
+
const failed = ref(false);
|
|
64
76
|
const height = ref("500px");
|
|
65
77
|
const selected = ref<string[]>([]);
|
|
66
78
|
|
|
@@ -72,20 +84,40 @@ const headers = ref([
|
|
|
72
84
|
{ title: "Date", value: "createdAt" },
|
|
73
85
|
]);
|
|
74
86
|
|
|
87
|
+
// `getWorkOrders` RETURNS the reply; nothing was ever reading it. `workOrders`,
|
|
88
|
+
// `pages` and `pageRange` are `useState` cells that no code in this layer or in
|
|
89
|
+
// any of the eleven apps ever assigned, so this table drew zero rows and
|
|
90
|
+
// "-- - -- of --" no matter what the server sent. The reply is now bound
|
|
91
|
+
// through the one helper that knows how a paginated reply is shaped, so the
|
|
92
|
+
// count on screen is the server's count of the whole list - never the number
|
|
93
|
+
// of rows on this page.
|
|
75
94
|
async function _getWorkOrders() {
|
|
76
95
|
loading.value = true;
|
|
96
|
+
failed.value = false;
|
|
77
97
|
|
|
78
98
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
const listPage = readListPage<TWorkOrder>(
|
|
100
|
+
await getWorkOrders({
|
|
101
|
+
page: page.value,
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
workOrders.value = listPage.items;
|
|
105
|
+
pages.value = listPage.pages;
|
|
106
|
+
pageRange.value = listPage.pageRange;
|
|
82
107
|
|
|
83
108
|
emit("update:pagination", {
|
|
84
109
|
page: page.value,
|
|
85
110
|
pages: pages.value,
|
|
86
111
|
});
|
|
87
112
|
} catch (error) {
|
|
88
|
-
|
|
113
|
+
// Say the request failed rather than presenting it as an empty list. The
|
|
114
|
+
// count goes back to the placeholder for the same reason: a failure is not
|
|
115
|
+
// a zero.
|
|
116
|
+
failed.value = true;
|
|
117
|
+
workOrders.value = [];
|
|
118
|
+
pages.value = 0;
|
|
119
|
+
pageRange.value = NO_PAGE_RANGE;
|
|
120
|
+
console.error("Error fetching work orders:", error);
|
|
89
121
|
} finally {
|
|
90
122
|
loading.value = false;
|
|
91
123
|
}
|
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.
|
|
5
|
+
"version": "3.2.2-staging.194",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
package/utils/data.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
|
|
4
|
-
import { findServiceProviderIdBySite } from "./data.ts";
|
|
4
|
+
import { findServiceProviderIdBySite, levelCount } from "./data.ts";
|
|
5
5
|
|
|
6
6
|
const SITE = "68f05fcc0c11062938dadada";
|
|
7
7
|
const OTHER_SITE = "68f05fcc0c11062938d00001";
|
|
@@ -70,3 +70,26 @@ test("an account with no provider list still gets the sentence, not a TypeError"
|
|
|
70
70
|
(error: any) => error.constructor === Error && !/undefined/.test(error.message)
|
|
71
71
|
);
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A block whose record carries no `levels` array used to throw during render
|
|
76
|
+
* (`Cannot read properties of undefined (reading 'length')`), which blanks the
|
|
77
|
+
* entire blocks table - not just the offending row.
|
|
78
|
+
*/
|
|
79
|
+
test("a block with no levels field counts as zero instead of throwing", () => {
|
|
80
|
+
assert.equal(levelCount({ levels: undefined }), 0);
|
|
81
|
+
assert.equal(levelCount({}), 0);
|
|
82
|
+
assert.equal(levelCount(undefined), 0);
|
|
83
|
+
assert.equal(levelCount(null), 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a block with no levels field is not confused with a block that has none", () => {
|
|
87
|
+
assert.equal(levelCount({ levels: [] }), 0);
|
|
88
|
+
assert.equal(levelCount({ levels: [{ name: "1" }, { name: "2" }] }), 2);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("a non-array levels value is counted as zero, not as its own length", () => {
|
|
92
|
+
// The API has been seen to answer with a count rather than the array.
|
|
93
|
+
assert.equal(levelCount({ levels: 7 as unknown as [] }), 0);
|
|
94
|
+
assert.equal(levelCount({ levels: "12" as unknown as [] }), 0);
|
|
95
|
+
});
|
package/utils/data.ts
CHANGED
|
@@ -160,3 +160,18 @@ export const generateRefNumber = () => {
|
|
|
160
160
|
const ref = (timestamp + random).substring(0, 10).toUpperCase();
|
|
161
161
|
return ref;
|
|
162
162
|
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* HOW MANY LEVELS A BLOCK HAS.
|
|
166
|
+
*
|
|
167
|
+
* `levels` is optional on a building record - a block created before levels
|
|
168
|
+
* were captured, or one the list endpoint returns without them, simply has no
|
|
169
|
+
* such field. Two screens read `building.levels.length` directly, which throws
|
|
170
|
+
* a TypeError DURING RENDER: Vue tears the component down and the whole blocks
|
|
171
|
+
* table disappears, so one incomplete row blanks every other row with it.
|
|
172
|
+
*
|
|
173
|
+
* Counting in one place means both screens - and anything added later - answer
|
|
174
|
+
* "no levels recorded" with a zero instead of taking the table down.
|
|
175
|
+
*/
|
|
176
|
+
export const levelCount = (building?: { levels?: unknown } | null): number =>
|
|
177
|
+
Array.isArray(building?.levels) ? building.levels.length : 0;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readListPage, NO_PAGE_RANGE } from "./list-page.ts";
|
|
4
|
+
|
|
5
|
+
const rows = (n: number, from = 1) =>
|
|
6
|
+
Array.from({ length: n }, (_, i) => ({ _id: `row-${from + i}` }));
|
|
7
|
+
|
|
8
|
+
describe("readListPage", () => {
|
|
9
|
+
it("keeps the server's total when it is bigger than the page - the failing case", () => {
|
|
10
|
+
// 47 announcements served 10 at a time. Deriving from the page gives
|
|
11
|
+
// "10" and one page, which is the defect this replaces.
|
|
12
|
+
const got = readListPage({ items: rows(10), pages: 5, pageRange: "1-10 of 47" });
|
|
13
|
+
|
|
14
|
+
assert.equal(got.items.length, 10);
|
|
15
|
+
assert.equal(got.pages, 5);
|
|
16
|
+
assert.equal(got.pageRange, "1-10 of 47");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("keeps the server's page 2 range rather than re-slicing", () => {
|
|
20
|
+
const got = readListPage({ items: rows(10, 11), pages: 5, pageRange: "11-20 of 47" });
|
|
21
|
+
|
|
22
|
+
assert.equal(got.items.length, 10);
|
|
23
|
+
assert.equal(got.items[0]._id, "row-11");
|
|
24
|
+
assert.equal(got.pageRange, "11-20 of 47");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("passes a single page through unchanged", () => {
|
|
28
|
+
const got = readListPage({ items: rows(7), pages: 1, pageRange: "1-7 of 7" });
|
|
29
|
+
|
|
30
|
+
assert.deepEqual(
|
|
31
|
+
{ pages: got.pages, pageRange: got.pageRange, n: got.items.length },
|
|
32
|
+
{ pages: 1, pageRange: "1-7 of 7", n: 7 }
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("reports a genuinely empty list as the server described it", () => {
|
|
37
|
+
const got = readListPage({ items: [], pages: 0, pageRange: "0 - 0 of 0" });
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(got, { items: [], pages: 0, pageRange: "0 - 0 of 0" });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("does not invent a zero when the request failed and there is no reply", () => {
|
|
43
|
+
for (const reply of [null, undefined]) {
|
|
44
|
+
const got = readListPage(reply);
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(got.items, []);
|
|
47
|
+
assert.equal(got.pages, 0);
|
|
48
|
+
// "0 of 0" would be a statement about the data. This is not one.
|
|
49
|
+
assert.equal(got.pageRange, NO_PAGE_RANGE);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not fall back to the page length when the envelope omits its totals", () => {
|
|
54
|
+
const got = readListPage({ items: rows(10) });
|
|
55
|
+
|
|
56
|
+
assert.equal(got.pages, 0);
|
|
57
|
+
assert.equal(got.pageRange, NO_PAGE_RANGE);
|
|
58
|
+
assert.notEqual(got.pageRange, "1-10 of 10");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* READING A PAGINATED LIST REPLY.
|
|
3
|
+
*
|
|
4
|
+
* Every paginated endpoint in this product answers with an envelope: the rows
|
|
5
|
+
* for the page that was asked for, plus `pages` and `pageRange` describing the
|
|
6
|
+
* WHOLE list. Screens kept re-deriving those two from the rows they had just
|
|
7
|
+
* been handed - `items.length` is the size of one page, not the size of the
|
|
8
|
+
* list - so a list of 47 announcements presented itself as 10 and its pager
|
|
9
|
+
* was told there was one page.
|
|
10
|
+
*
|
|
11
|
+
* This is the one place that reads the envelope, so the rule is stated once:
|
|
12
|
+
* take the server's own totals, and when the reply is missing (a failed
|
|
13
|
+
* request) say so with a placeholder rather than inventing a zero.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const NO_PAGE_RANGE = "-- - -- of --";
|
|
17
|
+
|
|
18
|
+
export type TListPageReply<T> = {
|
|
19
|
+
items?: T[] | null;
|
|
20
|
+
pages?: number | null;
|
|
21
|
+
pageRange?: string | null;
|
|
22
|
+
} | null | undefined;
|
|
23
|
+
|
|
24
|
+
export type TListPage<T> = {
|
|
25
|
+
items: T[];
|
|
26
|
+
pages: number;
|
|
27
|
+
pageRange: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function readListPage<T>(reply: TListPageReply<T>): TListPage<T> {
|
|
31
|
+
return {
|
|
32
|
+
items: reply?.items ?? [],
|
|
33
|
+
// The server's page COUNT for the whole list. Never ceil(items/limit).
|
|
34
|
+
pages: reply?.pages ?? 0,
|
|
35
|
+
// The server's own "1-10 of 47" string. Never rebuilt from the page.
|
|
36
|
+
pageRange: reply?.pageRange || NO_PAGE_RANGE,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<v-row no-gutters align="center" justify="center">
|
|
3
|
-
<v-col cols="12" lg="10">
|
|
4
|
-
<TableMain
|
|
5
|
-
:title="'Schedule Tasks'"
|
|
6
|
-
:items="items"
|
|
7
|
-
:headers="headers"
|
|
8
|
-
:loading="loading"
|
|
9
|
-
:show-header="true"
|
|
10
|
-
v-model:page="page"
|
|
11
|
-
:pages="pages"
|
|
12
|
-
:pageRange="pageRange"
|
|
13
|
-
:no-data-text="'No Schedule Tasks found'"
|
|
14
|
-
@row-click="onRowClick"
|
|
15
|
-
>
|
|
16
|
-
<template #item.task="{ item }">
|
|
17
|
-
{{ item.task || "N/A" }}
|
|
18
|
-
</template>
|
|
19
|
-
|
|
20
|
-
<template #item.createdAt="{ value }">
|
|
21
|
-
{{
|
|
22
|
-
formatDate
|
|
23
|
-
? formatDate(value)
|
|
24
|
-
: value
|
|
25
|
-
? new Date(value).toLocaleDateString()
|
|
26
|
-
: ""
|
|
27
|
-
}}
|
|
28
|
-
</template>
|
|
29
|
-
|
|
30
|
-
<template #item.area="{ value }">
|
|
31
|
-
{{ value || "N/A" }}
|
|
32
|
-
</template>
|
|
33
|
-
|
|
34
|
-
<template #item.createdBy="{ item }">
|
|
35
|
-
<!--
|
|
36
|
-
THE 3.12:1 `HS` CHIP.
|
|
37
|
-
|
|
38
|
-
This was a hand-rolled copy of `AvatarMain` with two problems: its
|
|
39
|
-
fill was Vuetify's MATERIAL `blue`/`purple`/`grey` (not a theme
|
|
40
|
-
token, so no `on-*` pair applied to it) and its `white--text` is
|
|
41
|
-
Vuetify 2 syntax that does nothing in Vuetify 3 - leaving Vuetify to
|
|
42
|
-
derive WHITE, which reads 3.12:1 on that blue in BOTH themes.
|
|
43
|
-
`AvatarMain` draws the same circle with the same initials and now
|
|
44
|
-
guarantees its label clears 4.5:1.
|
|
45
|
-
-->
|
|
46
|
-
<AvatarMain :name="item.createdBy" :size="32" class="mr-2" />
|
|
47
|
-
{{ item.createdBy || "N/A" }}
|
|
48
|
-
</template>
|
|
49
|
-
|
|
50
|
-
<template #item.cleaners="{ value }">
|
|
51
|
-
{{ value || "N/A" }}
|
|
52
|
-
</template>
|
|
53
|
-
|
|
54
|
-
<template #item.status="{ value }">
|
|
55
|
-
<StatusChip :status="value || 'To-Do'" :label="value || 'To-Do'" />
|
|
56
|
-
</template>
|
|
57
|
-
<!-- Completion Date/Time -->
|
|
58
|
-
<template #item.completionDate="{ value }">
|
|
59
|
-
{{ value ? formatDate(value) : "N/A" }}
|
|
60
|
-
</template>
|
|
61
|
-
<!-- Action (Accept button) -->
|
|
62
|
-
<template #item.action="{ item }">
|
|
63
|
-
<v-btn
|
|
64
|
-
variant="outlined"
|
|
65
|
-
class="text-none screen-btn-ghost"
|
|
66
|
-
@click.stop="acceptTask(item)"
|
|
67
|
-
>
|
|
68
|
-
Accept
|
|
69
|
-
</v-btn>
|
|
70
|
-
</template>
|
|
71
|
-
</TableMain>
|
|
72
|
-
|
|
73
|
-
<!-- Accept Confirmation Dialog -->
|
|
74
|
-
<AcceptDialog
|
|
75
|
-
:model-value="showAcceptDialog"
|
|
76
|
-
title="Accept Scheduled Task"
|
|
77
|
-
message="Are you sure you want to Accept this Scheduled Task?"
|
|
78
|
-
@confirm="confirmAccept"
|
|
79
|
-
@cancel="showAcceptDialog = false"
|
|
80
|
-
/>
|
|
81
|
-
|
|
82
|
-
<!-- ScheduleTaskForm Dialog -->
|
|
83
|
-
<ScheduleTaskForm v-model:show="showForm" />
|
|
84
|
-
</v-col>
|
|
85
|
-
</v-row>
|
|
86
|
-
</template>
|
|
87
|
-
|
|
88
|
-
<script lang="ts" setup>
|
|
89
|
-
const props = defineProps({
|
|
90
|
-
orgId: { type: String, required: true },
|
|
91
|
-
site: { type: String, required: true },
|
|
92
|
-
type: { type: String, required: true },
|
|
93
|
-
});
|
|
94
|
-
const loading = ref(false);
|
|
95
|
-
const page = ref(1);
|
|
96
|
-
const pages = ref(1);
|
|
97
|
-
const pageRange = ref("1-3 of 3");
|
|
98
|
-
const showForm = ref(false);
|
|
99
|
-
|
|
100
|
-
const showAcceptDialog = ref(false);
|
|
101
|
-
const selectedTask = ref(null);
|
|
102
|
-
|
|
103
|
-
const items = ref([
|
|
104
|
-
{
|
|
105
|
-
_id: "1",
|
|
106
|
-
task: "Clean Windows",
|
|
107
|
-
createdAt: "2025-10-28T10:00:00Z",
|
|
108
|
-
area: "",
|
|
109
|
-
createdBy: "Demo Smith",
|
|
110
|
-
cleaners: "N/A",
|
|
111
|
-
status: "To-Do",
|
|
112
|
-
completionDate: "2025-10-28T10:00:00Z",
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
_id: "2",
|
|
116
|
-
task: "Clean The 3",
|
|
117
|
-
createdAt: "2025-10-28T10:00:00Z",
|
|
118
|
-
area: "",
|
|
119
|
-
createdBy: "Hygiene SP",
|
|
120
|
-
cleaners: "N/A",
|
|
121
|
-
status: "To-Do",
|
|
122
|
-
completionDate: "2025-10-28T10:00:00Z",
|
|
123
|
-
},
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
const headers = [
|
|
127
|
-
{ title: "Task", value: "task" },
|
|
128
|
-
{ title: "Date Created", value: "createdAt" },
|
|
129
|
-
{ title: "Area", value: "area" },
|
|
130
|
-
{ title: "Created By", value: "createdBy" },
|
|
131
|
-
{ title: "Cleaners", value: "cleaners" },
|
|
132
|
-
{ title: "Status", value: "status" },
|
|
133
|
-
{ title: "Completion Date/Time", value: "completionDate" },
|
|
134
|
-
{ title: "Action", value: "action" },
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
const { formatDate } = useUtils();
|
|
138
|
-
|
|
139
|
-
function acceptTask(item: any) {
|
|
140
|
-
selectedTask.value = item;
|
|
141
|
-
showAcceptDialog.value = true;
|
|
142
|
-
}
|
|
143
|
-
function confirmAccept() {
|
|
144
|
-
// Handle accept logic here (e.g., update status, API call)
|
|
145
|
-
showAcceptDialog.value = false;
|
|
146
|
-
}
|
|
147
|
-
function onRowClick(data: any) {
|
|
148
|
-
const item = data?.item ?? data;
|
|
149
|
-
const id = item?._id || item?.id;
|
|
150
|
-
if (id) {
|
|
151
|
-
const path = `/${props.orgId}/${props.site}/schedule-tasks-ticket/${id}`;
|
|
152
|
-
navigateTo(path);
|
|
153
|
-
} else {
|
|
154
|
-
console.warn("Row clicked but no id found to navigate:", item);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
</script>
|