@7365admin1/layer-common 4.132.0 → 4.134.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -4
- package/components/AreaChecklistHistoryLogs.vue +5 -4
- package/components/CleaningScheduleMain.vue +109 -15
- package/components/FeedbackMain.vue +17 -1
- package/components/ManageChecklistMain.vue +16 -12
- package/components/RolePermissionFormCreate.vue +5 -1
- package/components/RolePermissionFormPreviewUpdate.vue +34 -4
- package/components/RolePermissionMain.vue +3 -1
- package/components/ScheduleAreaMain.vue +56 -71
- package/components/ScheduleGuide.vue +132 -0
- package/components/ScheduleTaskForm.vue +520 -353
- package/components/ScheduleTaskMain.vue +211 -70
- package/components/TableHygiene.vue +2 -2
- package/components/VehicleAddSelection.vue +5 -1
- package/components/VehicleManagement.vue +10 -4
- package/composables/useCleaningSchedules.ts +9 -0
- package/composables/useFeedback.ts +4 -0
- package/composables/useScheduleTask.ts +32 -2
- package/composables/useSecurityPermission.ts +19 -0
- package/constants/permissions.ts +7 -0
- package/package.json +1 -1
- package/types/cleaner-schedule.d.ts +1 -1
- package/utils/org-modules.ts +17 -1
- package/utils/schedule-task.ts +281 -0
- package/utils/security-catalogue.ts +59 -0
- package/utils/status.ts +6 -0
- package/utils/vehicle-actions.ts +10 -2
- package/composables/useScheduleTaskArea.ts +0 -85
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.134.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bd2c26d: Feedback list: optional category filter. `useFeedback().getFeedbacks({ category })` sends `category` (one subject or a comma list) to `/api/feedbacks2`; empty or absent sends nothing, so the list is unchanged. `FeedbackMain` gains a "Filter by category" box (default All).
|
|
8
|
+
- f5ed863: Schedule screens: clearer process for all five trade apps. Extra Visits (Schedule Task) now has a four-step form (What, Where, When, Who is told) with Once / Daily / Weekdays / Weekly / Monthly repeat, start and end date, and a summary before saving; the list shows When, Next run, Last run and Active / Paused / Ended with Edit, Pause/Resume and Delete. Daily Schedule: intro line, "Before you start" banner (Areas, Units, visits per day), "Create today's list now" only when today's list is missing, Closed filter, PDF error message. Wording: Done / Needs redo, In progress, Missed in history, Extra visit chip. Removed the dead Accept column and the unused useScheduleTaskArea composable.
|
|
9
|
+
|
|
10
|
+
## 4.133.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 2cfd359: Security roles: new permission boxes - See Blocklist, Block / Unblock Vehicles, Invite Visitor / Contractor, See Offline Data (needs core with the same keys). A security role gets a hidden `catalogue:security-v2` marker the first time it is saved in the editor; roles never saved keep today's access (the new boxes are pre-ticked from the old keys when opened). The marker is not counted in permission totals. `VehicleManagement` / `VehicleAddSelection` take optional `canSeeBlocklist` / `canManageBlocklist` props (default true = unchanged). Helper: `utils/security-catalogue`.
|
|
15
|
+
|
|
3
16
|
## 4.132.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
|
@@ -3346,11 +3359,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3346
3359
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3347
3360
|
own rule from two endpoints the console already calls, unprojected:
|
|
3348
3361
|
|
|
3349
|
-
|
|
3350
|
-
|
|
3362
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3363
|
+
GET /api/roles/id/:role that membership's role document
|
|
3351
3364
|
|
|
3352
|
-
|
|
3353
|
-
|
|
3365
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3366
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3354
3367
|
|
|
3355
3368
|
`role.default` is the marker because it is the only property of a platform
|
|
3356
3369
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
<StatusChip
|
|
50
50
|
class="text-capitalize"
|
|
51
51
|
:status="status"
|
|
52
|
-
:label="status
|
|
52
|
+
:label="status === 'N/A' ? status : dailyStatusLabel(status)"
|
|
53
53
|
/>
|
|
54
54
|
</v-col>
|
|
55
55
|
</v-row>
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
pill
|
|
126
126
|
class="ml-2"
|
|
127
127
|
>
|
|
128
|
-
|
|
128
|
+
Extra visit
|
|
129
129
|
</StatusChip>
|
|
130
130
|
</v-list-subheader>
|
|
131
131
|
|
|
@@ -165,8 +165,8 @@
|
|
|
165
165
|
green as on every other screen. -->
|
|
166
166
|
<StatusChip
|
|
167
167
|
class="text-capitalize"
|
|
168
|
-
:status="unit.status
|
|
169
|
-
:label="unit.status
|
|
168
|
+
:status="historyUnitLabel(unit.status)"
|
|
169
|
+
:label="historyUnitLabel(unit.status)"
|
|
170
170
|
/>
|
|
171
171
|
</v-col>
|
|
172
172
|
</v-row>
|
|
@@ -262,6 +262,7 @@
|
|
|
262
262
|
</template>
|
|
263
263
|
|
|
264
264
|
<script setup lang="ts">
|
|
265
|
+
import { dailyStatusLabel, historyUnitLabel } from "../utils/schedule-task";
|
|
265
266
|
const props = defineProps({
|
|
266
267
|
orgId: { type: String, required: true, default: "" },
|
|
267
268
|
site: { type: String, required: true, default: "" },
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters align="center" justify="center">
|
|
3
3
|
<v-col cols="12" lg="12">
|
|
4
|
+
<ScheduleGuide
|
|
5
|
+
v-if="canViewSchedules"
|
|
6
|
+
:org-id="orgId"
|
|
7
|
+
:site="site"
|
|
8
|
+
:service-type="serviceType"
|
|
9
|
+
:schedule-route="scheduleRoute"
|
|
10
|
+
text="A new checklist is created for every area automatically each midnight. Staff tick off each unit on the mobile app."
|
|
11
|
+
/>
|
|
4
12
|
<TableMain
|
|
5
13
|
v-if="canViewSchedules"
|
|
6
|
-
:title="
|
|
14
|
+
:title="pageTitle"
|
|
7
15
|
:items="items"
|
|
8
16
|
:headers="headers"
|
|
9
17
|
:loading="loading"
|
|
@@ -11,10 +19,31 @@
|
|
|
11
19
|
v-model:page="page"
|
|
12
20
|
:pages="pages"
|
|
13
21
|
:pageRange="pageRange"
|
|
14
|
-
:no-data-text="`No ${checklistTitle} found`"
|
|
15
22
|
@refresh="getCleanerChecklistRefresh"
|
|
16
23
|
@row-click="onRowClick"
|
|
17
24
|
>
|
|
25
|
+
<template #no-data>
|
|
26
|
+
<div class="cs-empty">
|
|
27
|
+
<strong>No daily lists yet.</strong>
|
|
28
|
+
<span>
|
|
29
|
+
A list is created automatically every midnight, once your Areas
|
|
30
|
+
and Units are set up.
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<template #actions>
|
|
36
|
+
<v-btn
|
|
37
|
+
v-if="showCreateToday"
|
|
38
|
+
class="text-none screen-btn-primary"
|
|
39
|
+
variant="flat"
|
|
40
|
+
:loading="creatingToday"
|
|
41
|
+
@click="createTodaysList"
|
|
42
|
+
>
|
|
43
|
+
Create today's list now
|
|
44
|
+
</v-btn>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
18
47
|
<template #extension>
|
|
19
48
|
<v-row no-gutters class="w-100 d-flex flex-column">
|
|
20
49
|
<div class="filter-bar">
|
|
@@ -71,7 +100,7 @@
|
|
|
71
100
|
<StatusChip
|
|
72
101
|
class="text-capitalize"
|
|
73
102
|
:status="value"
|
|
74
|
-
:label="value
|
|
103
|
+
:label="dailyStatusLabel(value)"
|
|
75
104
|
/>
|
|
76
105
|
</template>
|
|
77
106
|
<template #item.completedAt="{ value }">
|
|
@@ -108,16 +137,22 @@
|
|
|
108
137
|
</div>
|
|
109
138
|
</AppCard>
|
|
110
139
|
</div>
|
|
140
|
+
<Snackbar
|
|
141
|
+
v-model="messageSnackbar"
|
|
142
|
+
:text="message"
|
|
143
|
+
:color="messageColor"
|
|
144
|
+
/>
|
|
111
145
|
</v-col>
|
|
112
146
|
</v-row>
|
|
113
147
|
</template>
|
|
114
148
|
|
|
115
149
|
<script lang="ts" setup>
|
|
116
150
|
import useCleaningSchedules from "../composables/useCleaningSchedules";
|
|
117
|
-
import
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
151
|
+
import {
|
|
152
|
+
dailyStatusLabel,
|
|
153
|
+
hasListForToday,
|
|
154
|
+
scheduleNameFor,
|
|
155
|
+
} from "../utils/schedule-task";
|
|
121
156
|
import useUtils from "../composables/useUtils";
|
|
122
157
|
|
|
123
158
|
const props = defineProps({
|
|
@@ -130,16 +165,24 @@ const props = defineProps({
|
|
|
130
165
|
canViewScheduleDetails: { type: Boolean, default: false },
|
|
131
166
|
canDownloadSchedule: { type: Boolean, default: false },
|
|
132
167
|
canManageScheduleTasks: { type: Boolean, default: false },
|
|
168
|
+
canGenerateChecklist: { type: Boolean, default: false },
|
|
133
169
|
});
|
|
134
170
|
|
|
171
|
+
// "Daily Schedule - Cleaning" / "- Landscape" / "- Pest" / "- Pool" / "- Technician"
|
|
172
|
+
const pageTitle = `Daily Schedule – ${scheduleNameFor(props.serviceType).title.replace(
|
|
173
|
+
/ Schedule$/,
|
|
174
|
+
"",
|
|
175
|
+
)}`;
|
|
176
|
+
|
|
135
177
|
const startDate = ref("");
|
|
136
178
|
const endDate = ref("");
|
|
137
179
|
const status = ref<TScheduleAreaStatus>("all");
|
|
138
180
|
const statusOptions = [
|
|
139
181
|
{ title: "All", value: "all" },
|
|
140
182
|
{ title: "Open", value: "open" },
|
|
141
|
-
{ title: "
|
|
183
|
+
{ title: "In progress", value: "ongoing" },
|
|
142
184
|
{ title: "Completed", value: "completed" },
|
|
185
|
+
{ title: "Closed", value: "closed" },
|
|
143
186
|
] as const;
|
|
144
187
|
|
|
145
188
|
const items = ref<Array<Record<string, any>>>([]);
|
|
@@ -157,8 +200,18 @@ const headers = [
|
|
|
157
200
|
const remainingTime = ref({} as Record<string, string>);
|
|
158
201
|
const remainingSeconds = ref({} as Record<string, number>);
|
|
159
202
|
const downloadingId = ref<string | null>(null);
|
|
203
|
+
const creatingToday = ref(false);
|
|
160
204
|
|
|
161
|
-
const
|
|
205
|
+
const message = ref("");
|
|
206
|
+
const messageSnackbar = ref(false);
|
|
207
|
+
const messageColor = ref("error");
|
|
208
|
+
|
|
209
|
+
const {
|
|
210
|
+
getCleaningSchedules,
|
|
211
|
+
downloadChecklistPdf,
|
|
212
|
+
createTodaysParent,
|
|
213
|
+
generateChecklist,
|
|
214
|
+
} = useCleaningSchedules();
|
|
162
215
|
const { formatDate } = useUtils();
|
|
163
216
|
|
|
164
217
|
/**
|
|
@@ -233,6 +286,36 @@ const updateRemainingTime = () => {
|
|
|
233
286
|
});
|
|
234
287
|
};
|
|
235
288
|
|
|
289
|
+
// Only on the unfiltered first page, where today's row would be if it exists.
|
|
290
|
+
const showCreateToday = computed(
|
|
291
|
+
() =>
|
|
292
|
+
props.canGenerateChecklist &&
|
|
293
|
+
!loading.value &&
|
|
294
|
+
!!getCleanerChecklistReq.value &&
|
|
295
|
+
page.value === 1 &&
|
|
296
|
+
!startDate.value &&
|
|
297
|
+
!endDate.value &&
|
|
298
|
+
status.value === "all" &&
|
|
299
|
+
!hasListForToday(items.value),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
async function createTodaysList() {
|
|
303
|
+
try {
|
|
304
|
+
creatingToday.value = true;
|
|
305
|
+
const parent = await createTodaysParent(props.site, props.serviceType);
|
|
306
|
+
await generateChecklist(props.site, parent.id);
|
|
307
|
+
showMessage("Today's list is ready.", "success");
|
|
308
|
+
await getCleanerChecklistRefresh();
|
|
309
|
+
} catch (error: any) {
|
|
310
|
+
showMessage(
|
|
311
|
+
error?.data?.message || error?.message || "Could not create today's list.",
|
|
312
|
+
"error",
|
|
313
|
+
);
|
|
314
|
+
} finally {
|
|
315
|
+
creatingToday.value = false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
236
319
|
watchEffect(() => {
|
|
237
320
|
if (getCleanerChecklistReq.value) {
|
|
238
321
|
items.value = getCleanerChecklistReq.value.items;
|
|
@@ -286,7 +369,11 @@ async function downloadItem(item: any) {
|
|
|
286
369
|
downloadingId.value = id;
|
|
287
370
|
await downloadChecklistPdf(id);
|
|
288
371
|
} catch (err) {
|
|
289
|
-
|
|
372
|
+
showMessage(
|
|
373
|
+
(err as any)?.data?.message ||
|
|
374
|
+
"Could not download the PDF. Please try again.",
|
|
375
|
+
"error",
|
|
376
|
+
);
|
|
290
377
|
} finally {
|
|
291
378
|
downloadingId.value = null;
|
|
292
379
|
}
|
|
@@ -328,15 +415,22 @@ function onRowClick(data: any) {
|
|
|
328
415
|
}
|
|
329
416
|
|
|
330
417
|
function showMessage(msg: string, color: string = "error") {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
alert(msg);
|
|
335
|
-
}
|
|
418
|
+
message.value = msg;
|
|
419
|
+
messageColor.value = color;
|
|
420
|
+
messageSnackbar.value = true;
|
|
336
421
|
}
|
|
337
422
|
</script>
|
|
338
423
|
|
|
339
424
|
<style scoped>
|
|
425
|
+
.cs-empty {
|
|
426
|
+
display: flex;
|
|
427
|
+
flex-direction: column;
|
|
428
|
+
gap: 4px;
|
|
429
|
+
padding: 24px 16px;
|
|
430
|
+
font-family: var(--font-sans);
|
|
431
|
+
font-size: var(--fs-cell);
|
|
432
|
+
color: var(--text2);
|
|
433
|
+
}
|
|
340
434
|
.cs-no-permission {
|
|
341
435
|
font-family: var(--font-sans);
|
|
342
436
|
font-size: var(--fs-cell);
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
class="ml-2"
|
|
29
29
|
style="max-width: 250px"
|
|
30
30
|
/>
|
|
31
|
+
<v-autocomplete
|
|
32
|
+
v-model="filterByCategory"
|
|
33
|
+
:items="categoryFilterItems"
|
|
34
|
+
label="Filter by category"
|
|
35
|
+
hide-details
|
|
36
|
+
class="ml-2"
|
|
37
|
+
style="max-width: 250px"
|
|
38
|
+
/>
|
|
31
39
|
<v-text-field
|
|
32
40
|
v-model="startDate"
|
|
33
41
|
:value="standardFormatDate(startDate)"
|
|
@@ -684,6 +692,7 @@ const pageRange = ref("-- - -- of --");
|
|
|
684
692
|
const items = ref<Array<Record<string, any>>>([]);
|
|
685
693
|
|
|
686
694
|
const filterByStatus = ref("All");
|
|
695
|
+
const filterByCategory = ref("All");
|
|
687
696
|
const getPast30DaysDate = (): Date => {
|
|
688
697
|
const today = new Date();
|
|
689
698
|
const past30Days = new Date(today);
|
|
@@ -715,9 +724,10 @@ const {
|
|
|
715
724
|
dateTo: moment(endDate.value, "DD/MM/YYYY").endOf("day"),
|
|
716
725
|
search: searchText.value,
|
|
717
726
|
status: filterByStatus.value == "All" ? "" : filterByStatus.value,
|
|
727
|
+
category: filterByCategory.value == "All" ? "" : filterByCategory.value,
|
|
718
728
|
}),
|
|
719
729
|
{
|
|
720
|
-
watch: [filterByStatus, searchText, page, startDate, endDate],
|
|
730
|
+
watch: [filterByStatus, filterByCategory, searchText, page, startDate, endDate],
|
|
721
731
|
}
|
|
722
732
|
);
|
|
723
733
|
|
|
@@ -1068,6 +1078,12 @@ const subjectOptions = computed(() => {
|
|
|
1068
1078
|
});
|
|
1069
1079
|
});
|
|
1070
1080
|
|
|
1081
|
+
// Same categories as the create form, plus "All" (default: nothing hidden).
|
|
1082
|
+
const categoryFilterItems = computed(() => [
|
|
1083
|
+
"All",
|
|
1084
|
+
...subjectOptions.value.map((item) => item.title),
|
|
1085
|
+
]);
|
|
1086
|
+
|
|
1071
1087
|
const isFileUploading = ref<boolean>(false);
|
|
1072
1088
|
type TFile = {
|
|
1073
1089
|
name: string;
|
|
@@ -109,9 +109,9 @@
|
|
|
109
109
|
<div class="d-flex align-center ga-2 mb-2">
|
|
110
110
|
<v-icon color="error" size="18">mdi-close-circle</v-icon>
|
|
111
111
|
<span class="text-caption font-weight-bold">
|
|
112
|
-
|
|
112
|
+
Needs redo
|
|
113
113
|
</span>
|
|
114
|
-
<StatusChip status="
|
|
114
|
+
<StatusChip status="Needs redo" :dot="false" />
|
|
115
115
|
</div>
|
|
116
116
|
<div
|
|
117
117
|
v-if="item.remarks"
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
</v-row>
|
|
147
147
|
</template>
|
|
148
148
|
|
|
149
|
-
<!--
|
|
149
|
+
<!-- Done / Needs redo action buttons (labels only; the API still says approve / reject) -->
|
|
150
150
|
<template #item-append="{ item, group }">
|
|
151
151
|
<v-row
|
|
152
152
|
v-if="!isScheduleClosed && canManageScheduleTasks"
|
|
@@ -165,6 +165,8 @@
|
|
|
165
165
|
color="error"
|
|
166
166
|
:loading="!!loadingActions[getKey(item, group.set)]"
|
|
167
167
|
:disabled="!!loadingActions[getKey(item, group.set)]"
|
|
168
|
+
aria-label="Needs redo"
|
|
169
|
+
title="Needs redo"
|
|
168
170
|
@click.stop="handleItemActionClick(item, group.set, 'reject')"
|
|
169
171
|
/>
|
|
170
172
|
</v-col>
|
|
@@ -180,6 +182,8 @@
|
|
|
180
182
|
color="success"
|
|
181
183
|
:loading="!!loadingActions[getKey(item, group.set)]"
|
|
182
184
|
:disabled="!!loadingActions[getKey(item, group.set)]"
|
|
185
|
+
aria-label="Done"
|
|
186
|
+
title="Done"
|
|
183
187
|
@click.stop="handleItemActionClick(item, group.set, 'approve')"
|
|
184
188
|
/>
|
|
185
189
|
</v-col>
|
|
@@ -190,7 +194,7 @@
|
|
|
190
194
|
<v-row no-gutters align="center" class="ga-2">
|
|
191
195
|
<v-col v-if="isGroupAnyApproved(group)" cols="auto">
|
|
192
196
|
<StatusChip
|
|
193
|
-
:status="isGroupComplete(group) ? 'Completed' : '
|
|
197
|
+
:status="isGroupComplete(group) ? 'Completed' : 'In progress'"
|
|
194
198
|
:tone="isGroupComplete(group) ? 'ok' : 'warn'"
|
|
195
199
|
:dot="false"
|
|
196
200
|
class="text-none"
|
|
@@ -202,7 +206,7 @@
|
|
|
202
206
|
: "mdi-progress-clock"
|
|
203
207
|
}}
|
|
204
208
|
</v-icon>
|
|
205
|
-
{{ isGroupComplete(group) ? "Completed" : "
|
|
209
|
+
{{ isGroupComplete(group) ? "Completed" : "In progress" }}
|
|
206
210
|
<template v-if="getGroupCompletedByName(group)">
|
|
207
211
|
· {{ getGroupCompletedByName(group) }}
|
|
208
212
|
</template>
|
|
@@ -316,7 +320,7 @@
|
|
|
316
320
|
-->
|
|
317
321
|
<v-dialog v-model="showRejectModal" max-width="600" persistent>
|
|
318
322
|
<v-card class="screen-modal">
|
|
319
|
-
<v-card-title>
|
|
323
|
+
<v-card-title>Needs redo</v-card-title>
|
|
320
324
|
|
|
321
325
|
<v-divider />
|
|
322
326
|
|
|
@@ -901,14 +905,14 @@ async function submitRejectDialog(): Promise<void> {
|
|
|
901
905
|
rejectModal.remarks.trim(),
|
|
902
906
|
rejectModal.photoIds
|
|
903
907
|
);
|
|
904
|
-
showMessage(response?.message || "
|
|
908
|
+
showMessage(response?.message || "Marked as needs redo", "success");
|
|
905
909
|
showRejectModal.value = false;
|
|
906
910
|
resetRejectModal();
|
|
907
911
|
await getUnitCleanerChecklistRefresh();
|
|
908
912
|
} catch (error: any) {
|
|
909
913
|
console.error("Error rejecting unit checklist:", error);
|
|
910
914
|
showMessage(
|
|
911
|
-
error?.data?.message || error?.message || "
|
|
915
|
+
error?.data?.message || error?.message || "Could not mark this unit as needs redo",
|
|
912
916
|
"error"
|
|
913
917
|
);
|
|
914
918
|
} finally {
|
|
@@ -985,7 +989,7 @@ async function _callUpdateUnitChecklist(
|
|
|
985
989
|
(s) => s.unit === item.unit && s.set === set
|
|
986
990
|
);
|
|
987
991
|
if (isApproved && action === "approve") {
|
|
988
|
-
showMessage("
|
|
992
|
+
showMessage("This unit is already done", "info");
|
|
989
993
|
return;
|
|
990
994
|
}
|
|
991
995
|
|
|
@@ -1000,7 +1004,7 @@ async function _callUpdateUnitChecklist(
|
|
|
1000
1004
|
);
|
|
1001
1005
|
showMessage(
|
|
1002
1006
|
response?.message ||
|
|
1003
|
-
`Unit ${action === "approve" ? "
|
|
1007
|
+
`Unit marked as ${action === "approve" ? "done" : "needs redo"}`,
|
|
1004
1008
|
"success"
|
|
1005
1009
|
);
|
|
1006
1010
|
await getUnitCleanerChecklistRefresh();
|
|
@@ -1071,13 +1075,13 @@ async function _updateUnitChecklist({
|
|
|
1071
1075
|
);
|
|
1072
1076
|
}
|
|
1073
1077
|
|
|
1074
|
-
showMessage(`Set ${setNumber}
|
|
1078
|
+
showMessage(`Set ${setNumber} done`, "success");
|
|
1075
1079
|
|
|
1076
1080
|
// Refresh data
|
|
1077
1081
|
await getUnitCleanerChecklistRefresh();
|
|
1078
1082
|
} catch (error: any) {
|
|
1079
1083
|
console.error("Error approving set:", error);
|
|
1080
|
-
showMessage(error?.message || "
|
|
1084
|
+
showMessage(error?.message || "Could not finish this set", "error");
|
|
1081
1085
|
} finally {
|
|
1082
1086
|
submitting.value = false;
|
|
1083
1087
|
}
|
|
@@ -283,6 +283,7 @@ import useRole from "../composables/useRole";
|
|
|
283
283
|
import useUtils from "../composables/useUtils";
|
|
284
284
|
import useSite from "../composables/useSite";
|
|
285
285
|
import { readApiError } from "../utils/subscription-form";
|
|
286
|
+
import { withSecurityMarker } from "../utils/security-catalogue";
|
|
286
287
|
import { applyRolePreset, ROLE_PRESETS, type TRolePreset } from "../utils/inventory-presets";
|
|
287
288
|
|
|
288
289
|
/** Shown after "Couldn't save:" when the server gave no reason (network, timeout). */
|
|
@@ -533,7 +534,10 @@ async function submit() {
|
|
|
533
534
|
try {
|
|
534
535
|
await createRole({
|
|
535
536
|
name: name.value,
|
|
536
|
-
|
|
537
|
+
// Security catalogue only (it carries `offline-data`): stamp the v2 marker.
|
|
538
|
+
permissions: "offline-data" in (props.permissions || {})
|
|
539
|
+
? withSecurityMarker(permState.value.selected)
|
|
540
|
+
: permState.value.selected,
|
|
537
541
|
type: props.type,
|
|
538
542
|
org: props.org,
|
|
539
543
|
site: props.siteId,
|
|
@@ -272,6 +272,11 @@ import {
|
|
|
272
272
|
import useRole from "../composables/useRole";
|
|
273
273
|
import useUtils from "../composables/useUtils";
|
|
274
274
|
import { readApiError } from "../utils/subscription-form";
|
|
275
|
+
import {
|
|
276
|
+
countRealPermissions,
|
|
277
|
+
securityV2PreTicks,
|
|
278
|
+
withSecurityMarker,
|
|
279
|
+
} from "../utils/security-catalogue";
|
|
275
280
|
import {
|
|
276
281
|
holdsPermission,
|
|
277
282
|
withoutPermission,
|
|
@@ -348,8 +353,8 @@ const totalLabel = computed(() => {
|
|
|
348
353
|
const held = definedModel.value ?? [];
|
|
349
354
|
if (holdsWildcard.value) return "All";
|
|
350
355
|
return props.catalogueSize > 0
|
|
351
|
-
? `${held
|
|
352
|
-
: String(held
|
|
356
|
+
? `${countRealPermissions(held)} / ${props.catalogueSize}`
|
|
357
|
+
: String(countRealPermissions(held));
|
|
353
358
|
});
|
|
354
359
|
|
|
355
360
|
/**
|
|
@@ -638,6 +643,23 @@ const edit = defineModel<boolean>("edit", {
|
|
|
638
643
|
default: false,
|
|
639
644
|
});
|
|
640
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Security catalogue only: the Security tree carries the `offline-data`
|
|
648
|
+
* module. Other apps' editors never match, so they are untouched.
|
|
649
|
+
*/
|
|
650
|
+
const isSecurityCatalogue = computed(
|
|
651
|
+
() => "offline-data" in (props.permissions as Record<string, unknown>)
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
// Opening an UNMARKED security role for edit: pre-tick the new boxes the role
|
|
655
|
+
// already effectively has (legacy fallback), so an unrelated save never strips
|
|
656
|
+
// access. Cancel restores the original list.
|
|
657
|
+
watch(edit, (on) => {
|
|
658
|
+
if (!on || !isSecurityCatalogue.value) return;
|
|
659
|
+
const add = securityV2PreTicks(definedModel.value ?? []);
|
|
660
|
+
if (add.length) definedModel.value = [...definedModel.value, ...add];
|
|
661
|
+
});
|
|
662
|
+
|
|
641
663
|
/**
|
|
642
664
|
* CANCEL THROWS THE DRAFT AWAY.
|
|
643
665
|
*
|
|
@@ -688,7 +710,10 @@ const errorMessages = computed(() => {
|
|
|
688
710
|
return equalValue;
|
|
689
711
|
}
|
|
690
712
|
|
|
691
|
-
|
|
713
|
+
// The marker is not a permission: a list holding only it is still empty.
|
|
714
|
+
const requiredValue = requireListRule(
|
|
715
|
+
(definedModel.value ?? []).filter((p) => countRealPermissions([p]) > 0)
|
|
716
|
+
);
|
|
692
717
|
if (requiredValue) {
|
|
693
718
|
return requiredValue;
|
|
694
719
|
}
|
|
@@ -706,7 +731,12 @@ async function submit() {
|
|
|
706
731
|
disable.value = true;
|
|
707
732
|
message.value = "";
|
|
708
733
|
try {
|
|
709
|
-
await updatePermissionById(
|
|
734
|
+
await updatePermissionById(
|
|
735
|
+
props.id,
|
|
736
|
+
isSecurityCatalogue.value
|
|
737
|
+
? withSecurityMarker(definedModel.value)
|
|
738
|
+
: definedModel.value
|
|
739
|
+
);
|
|
710
740
|
emit("success");
|
|
711
741
|
} catch (error: any) {
|
|
712
742
|
// `error.response` is undefined on a network or timeout failure, so the old
|
|
@@ -208,6 +208,7 @@
|
|
|
208
208
|
// Relative import: this layer's `utils/` is not auto-imported into consumers.
|
|
209
209
|
import { roleLabel, safeReturnTo, showSiteField, sitePickerRoleLabel } from "../utils/role";
|
|
210
210
|
import { holdsPermission } from "../utils/permission-spellings";
|
|
211
|
+
import { countRealPermissions } from "../utils/security-catalogue";
|
|
211
212
|
import {
|
|
212
213
|
catalogueForOrg,
|
|
213
214
|
catalogueForOwnerGrant,
|
|
@@ -570,7 +571,8 @@ function getPermissionCount(permissions: unknown): string {
|
|
|
570
571
|
if (held.includes("*")) return "All";
|
|
571
572
|
|
|
572
573
|
const offered = permissionCatalogueSize.value;
|
|
573
|
-
|
|
574
|
+
const count = countRealPermissions(held);
|
|
575
|
+
return offered > 0 ? `${count} / ${offered}` : String(count);
|
|
574
576
|
}
|
|
575
577
|
|
|
576
578
|
function updateRolePermissionsInList(
|