@7365admin1/layer-common 1.11.38 → 1.11.39
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 +6 -0
- package/components/CameraForm.vue +58 -13
- package/components/CameraMain.vue +19 -3
- package/components/ClientMain.vue +78 -32
- package/components/InvitationForm.vue +35 -5
- package/components/Layout/NavigationDrawer.vue +49 -26
- package/components/RolePermissionFormCreate.vue +106 -46
- package/components/ScheduleTaskMain.vue +105 -0
- package/components/VehicleForm.vue +5 -1
- package/components/VehicleManagement.vue +27 -17
- package/composables/useSiteSettings.ts +1 -0
- package/composables/useVehicle.ts +14 -1
- package/package.json +1 -1
- package/types/camera.d.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -11,12 +11,21 @@
|
|
|
11
11
|
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5">
|
|
12
12
|
<v-form ref="formRef" v-model="validForm" :disabled="disable">
|
|
13
13
|
<v-row no-gutters class="pt-4">
|
|
14
|
+
<v-col v-if="isCCTV" cols="12">
|
|
15
|
+
<InputLabel class="text-capitalize" title="Camera Name" required />
|
|
16
|
+
<v-text-field
|
|
17
|
+
v-model.trim="camera.name"
|
|
18
|
+
density="comfortable"
|
|
19
|
+
:rules="[requiredRule]"
|
|
20
|
+
placeholder="Enter CCTV camera name"
|
|
21
|
+
/>
|
|
22
|
+
</v-col>
|
|
14
23
|
<v-col cols="12">
|
|
15
24
|
<InputLabel class="text-capitalize" title="URL" required />
|
|
16
25
|
<v-text-field
|
|
17
26
|
v-model.trim="camera.host"
|
|
18
27
|
density="comfortable"
|
|
19
|
-
:rules="[requiredRule,
|
|
28
|
+
:rules="[requiredRule, hostRule]"
|
|
20
29
|
placeholder="Enter camera URL"
|
|
21
30
|
/>
|
|
22
31
|
</v-col>
|
|
@@ -32,7 +41,7 @@
|
|
|
32
41
|
</v-col>
|
|
33
42
|
|
|
34
43
|
<v-col v-if="isANPR" cols="12">
|
|
35
|
-
<InputLabel class="text-capitalize" title="
|
|
44
|
+
<InputLabel class="text-capitalize" title="Type" required />
|
|
36
45
|
<v-select
|
|
37
46
|
v-model="camera.direction"
|
|
38
47
|
:items="anprDirectionOptions"
|
|
@@ -50,7 +59,7 @@
|
|
|
50
59
|
/>
|
|
51
60
|
</v-col>
|
|
52
61
|
|
|
53
|
-
<v-col cols="12">
|
|
62
|
+
<v-col v-if="isANPR" cols="12">
|
|
54
63
|
<InputLabel class="text-capitalize" title="User" required />
|
|
55
64
|
<v-text-field
|
|
56
65
|
v-model.trim="camera.username"
|
|
@@ -60,7 +69,7 @@
|
|
|
60
69
|
/>
|
|
61
70
|
</v-col>
|
|
62
71
|
|
|
63
|
-
<v-col cols="12">
|
|
72
|
+
<v-col v-if="isANPR" cols="12">
|
|
64
73
|
<InputLabel class="text-capitalize" title="Password" required />
|
|
65
74
|
<v-text-field
|
|
66
75
|
v-model.trim="camera.password"
|
|
@@ -70,6 +79,20 @@
|
|
|
70
79
|
placeholder="Enter password"
|
|
71
80
|
/>
|
|
72
81
|
</v-col>
|
|
82
|
+
|
|
83
|
+
<v-col v-if="isANPR" cols="12">
|
|
84
|
+
<InputLabel class="text-capitalize" title="Status" />
|
|
85
|
+
<v-switch
|
|
86
|
+
v-model="camera.status"
|
|
87
|
+
hide-details
|
|
88
|
+
inset
|
|
89
|
+
density="comfortable"
|
|
90
|
+
true-value="active"
|
|
91
|
+
false-value="inactive"
|
|
92
|
+
:color="camera.status === 'active' ? 'success' : 'grey'"
|
|
93
|
+
:label="camera.status === 'active' ? 'Active' : 'Inactive'"
|
|
94
|
+
></v-switch>
|
|
95
|
+
</v-col>
|
|
73
96
|
</v-row>
|
|
74
97
|
</v-form>
|
|
75
98
|
</v-card-text>
|
|
@@ -107,7 +130,7 @@
|
|
|
107
130
|
</template>
|
|
108
131
|
|
|
109
132
|
<script setup lang="ts">
|
|
110
|
-
import useSiteSettings from
|
|
133
|
+
import useSiteSettings from "../composables/useSiteSettings";
|
|
111
134
|
|
|
112
135
|
const prop = defineProps({
|
|
113
136
|
title: {
|
|
@@ -139,9 +162,10 @@ const prop = defineProps({
|
|
|
139
162
|
password: "",
|
|
140
163
|
type: "ip",
|
|
141
164
|
category: "standard",
|
|
142
|
-
direction: "
|
|
165
|
+
direction: "both",
|
|
143
166
|
guardPost: null,
|
|
144
167
|
status: "active",
|
|
168
|
+
name: "",
|
|
145
169
|
}),
|
|
146
170
|
},
|
|
147
171
|
});
|
|
@@ -149,6 +173,23 @@ const prop = defineProps({
|
|
|
149
173
|
const { requiredRule, isValidBaseURL } = useUtils();
|
|
150
174
|
const emit = defineEmits(["cancel", "select", "success", "error"]);
|
|
151
175
|
|
|
176
|
+
const hostRule = computed(() => {
|
|
177
|
+
return (v: string | any) => {
|
|
178
|
+
if (!v) return true;
|
|
179
|
+
|
|
180
|
+
const isAnpr = prop.type === 'anpr';
|
|
181
|
+
|
|
182
|
+
const anprRegex = /^http:\/\/[a-z0-9.-]+\.[a-z]{2,}(:\d+)?$/i;
|
|
183
|
+
const defaultRegex = /^https:\/\/[a-z0-9.-]+\.[a-z]{2,}\/[a-z0-9._-]+$/i;
|
|
184
|
+
|
|
185
|
+
if (isAnpr) {
|
|
186
|
+
return anprRegex.test(v) || 'Format: http://domain:port';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return defaultRegex.test(v) || 'Format: https://domain/path';
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
152
193
|
const camera = ref<TSiteCamera>({
|
|
153
194
|
site: "",
|
|
154
195
|
host: "",
|
|
@@ -156,7 +197,8 @@ const camera = ref<TSiteCamera>({
|
|
|
156
197
|
password: "",
|
|
157
198
|
type: "ip",
|
|
158
199
|
category: "standard",
|
|
159
|
-
direction: "
|
|
200
|
+
direction: "both",
|
|
201
|
+
name: "",
|
|
160
202
|
guardPost: null,
|
|
161
203
|
status: "active",
|
|
162
204
|
});
|
|
@@ -166,11 +208,11 @@ camera.value.site = prop.site;
|
|
|
166
208
|
if (prop.mode === "edit") {
|
|
167
209
|
camera.value = JSON.parse(JSON.stringify(prop.camera));
|
|
168
210
|
}
|
|
169
|
-
|
|
211
|
+
console.log("prop.camera", prop?.camera);
|
|
170
212
|
camera.value.site = prop.site;
|
|
171
213
|
camera.value.type = prop.type;
|
|
172
|
-
camera.value.category = prop.type
|
|
173
|
-
camera.value.direction = prop.type
|
|
214
|
+
camera.value.category = prop.camera?.category && prop.title == "Edit Camera" ? prop.camera?.category : prop.type == "anpr" ? "resident" : "standard";
|
|
215
|
+
camera.value.direction = prop.camera?.direction && prop.title == "Edit Camera" ? prop.camera?.direction : prop.type == "anpr" ? "both" : "none";
|
|
174
216
|
|
|
175
217
|
const validForm = ref(false);
|
|
176
218
|
const formRef = ref<HTMLFormElement | null>(null);
|
|
@@ -178,6 +220,7 @@ const disable = ref(false);
|
|
|
178
220
|
const message = ref("");
|
|
179
221
|
|
|
180
222
|
const isANPR = computed(() => prop.type === "anpr");
|
|
223
|
+
const isCCTV = computed(() => prop.type === "ip");
|
|
181
224
|
|
|
182
225
|
const anprCategoryOptions = computed(() => [
|
|
183
226
|
{ title: "Resident", value: "resident" },
|
|
@@ -187,8 +230,7 @@ const anprCategoryOptions = computed(() => [
|
|
|
187
230
|
const anprDirectionOptions = computed(() => [
|
|
188
231
|
{ title: "Entry", value: "entry" },
|
|
189
232
|
{ title: "Exit", value: "exit" },
|
|
190
|
-
{ title: "Both", value: "both" },
|
|
191
|
-
{ title: "None", value: "none" },
|
|
233
|
+
{ title: "Both Entry and Exit", value: "both" },
|
|
192
234
|
]);
|
|
193
235
|
|
|
194
236
|
function back() {
|
|
@@ -210,7 +252,8 @@ const hasChanges = computed(() => {
|
|
|
210
252
|
camera.value.password !== prop.camera.password ||
|
|
211
253
|
camera.value.category !== prop.camera.category ||
|
|
212
254
|
camera.value.guardPost !== prop.camera.guardPost ||
|
|
213
|
-
camera.value.direction !== prop.camera.direction
|
|
255
|
+
camera.value.direction !== prop.camera.direction ||
|
|
256
|
+
camera.value.name !== prop.camera.name
|
|
214
257
|
);
|
|
215
258
|
}
|
|
216
259
|
|
|
@@ -237,6 +280,8 @@ async function submit() {
|
|
|
237
280
|
category: camera.value.category,
|
|
238
281
|
guardPost: camera.value.guardPost,
|
|
239
282
|
direction: camera.value.direction,
|
|
283
|
+
status: camera.value.status,
|
|
284
|
+
name: camera.value.name,
|
|
240
285
|
});
|
|
241
286
|
}
|
|
242
287
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
<v-card-text class="pa-0 bg-red">
|
|
26
26
|
<v-data-table
|
|
27
|
-
:headers="
|
|
27
|
+
:headers="computedHeaders"
|
|
28
28
|
:items="items"
|
|
29
29
|
fixed-header
|
|
30
30
|
hide-default-footer
|
|
@@ -92,6 +92,12 @@
|
|
|
92
92
|
class="pa-5 my-5 px-7"
|
|
93
93
|
>
|
|
94
94
|
<v-row no-gutters>
|
|
95
|
+
<v-col v-if="camera.type === 'ip'" cols="12" class="mb-2">
|
|
96
|
+
Camera Name:
|
|
97
|
+
<span class="font-weight-bold">
|
|
98
|
+
{{ camera.name }}
|
|
99
|
+
</span>
|
|
100
|
+
</v-col>
|
|
95
101
|
<v-col cols="12" class="mb-2">
|
|
96
102
|
Host:
|
|
97
103
|
<span class="font-weight-bold">
|
|
@@ -99,14 +105,14 @@
|
|
|
99
105
|
</span>
|
|
100
106
|
</v-col>
|
|
101
107
|
|
|
102
|
-
<v-col cols="12" class="mb-2">
|
|
108
|
+
<v-col v-if="camera.type === 'anpr'" cols="12" class="mb-2">
|
|
103
109
|
Username:
|
|
104
110
|
<span class="font-weight-bold">
|
|
105
111
|
{{ camera.username }}
|
|
106
112
|
</span>
|
|
107
113
|
</v-col>
|
|
108
114
|
|
|
109
|
-
<v-col cols="12" class="mb-2">
|
|
115
|
+
<v-col v-if="camera.type === 'anpr'" cols="12" class="mb-2">
|
|
110
116
|
Password:
|
|
111
117
|
<span class="font-weight-bold">
|
|
112
118
|
{{ camera.password }}
|
|
@@ -275,6 +281,15 @@ const prop = defineProps({
|
|
|
275
281
|
},
|
|
276
282
|
});
|
|
277
283
|
|
|
284
|
+
const computedHeaders = computed(() => {
|
|
285
|
+
if (prop.type === "ip") {
|
|
286
|
+
const base = [...prop.headers];
|
|
287
|
+
base.splice(0, 0, { text: "Name", value: "name" });
|
|
288
|
+
return base;
|
|
289
|
+
}
|
|
290
|
+
return prop.headers;
|
|
291
|
+
});
|
|
292
|
+
|
|
278
293
|
const items = ref<Array<TCamera>>([]);
|
|
279
294
|
const page = ref(1);
|
|
280
295
|
const pages = ref(0);
|
|
@@ -341,6 +356,7 @@ const camera = ref<TSiteCamera>({
|
|
|
341
356
|
});
|
|
342
357
|
|
|
343
358
|
function handleRowClick(_: any, data: any) {
|
|
359
|
+
console.log("row clicked", data.item);
|
|
344
360
|
dialogPreview.value = true;
|
|
345
361
|
camera.value = JSON.parse(JSON.stringify(data.item));
|
|
346
362
|
}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters>
|
|
3
3
|
<v-col cols="12">
|
|
4
|
-
<TableMain
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
<TableMain
|
|
5
|
+
|
|
6
|
+
:headers="currentHeaders"
|
|
7
|
+
:items="items"
|
|
8
|
+
:loading="loading"
|
|
9
|
+
:page="page"
|
|
10
|
+
:pages="pages"
|
|
11
|
+
:page-range="pageRange"
|
|
12
|
+
:items-per-page="limit"
|
|
13
|
+
:extension-height="120"
|
|
14
|
+
:offset="300"
|
|
15
|
+
show-header
|
|
16
|
+
@refresh="handleRefresh"
|
|
17
|
+
@update:page="handleUpdatePage"
|
|
18
|
+
>
|
|
7
19
|
<!-- EXTENSION -->
|
|
8
20
|
<template #extension>
|
|
9
21
|
<v-row no-gutters class="w-100 d-flex flex-column ga-2 pt-2">
|
|
@@ -144,10 +156,15 @@ async function resolveRoleName(id: string) {
|
|
|
144
156
|
roleCache.value[id] = roleName
|
|
145
157
|
return roleName
|
|
146
158
|
}
|
|
147
|
-
|
|
159
|
+
const limit = ref(10)
|
|
148
160
|
/* ================= FETCH ================= */
|
|
149
161
|
async function fetchActive() {
|
|
150
|
-
const res = await getAll({
|
|
162
|
+
const res = await getAll({
|
|
163
|
+
page: page.value,
|
|
164
|
+
search: search.value,
|
|
165
|
+
limit: limit.value,
|
|
166
|
+
})
|
|
167
|
+
|
|
151
168
|
const orgs = res?.data?.items || res?.items || []
|
|
152
169
|
|
|
153
170
|
const mapped = await Promise.all(
|
|
@@ -160,6 +177,7 @@ async function fetchActive() {
|
|
|
160
177
|
|
|
161
178
|
try {
|
|
162
179
|
const siteRes = await getCustomerSitesByOrgId(org._id)
|
|
180
|
+
|
|
163
181
|
sitesCount =
|
|
164
182
|
siteRes?.data?.totalItems ||
|
|
165
183
|
siteRes?.totalItems ||
|
|
@@ -169,6 +187,7 @@ async function fetchActive() {
|
|
|
169
187
|
|
|
170
188
|
const subRes = await getSubscriptionByOrgId(org._id)
|
|
171
189
|
const subscription = subRes?.data || subRes
|
|
190
|
+
|
|
172
191
|
if (subscription) {
|
|
173
192
|
planType = subscription?.paidType || '-'
|
|
174
193
|
billingCycle = subscription?.billingCycle || '-'
|
|
@@ -176,25 +195,33 @@ async function fetchActive() {
|
|
|
176
195
|
subscriptionEnd = formatDate(subscription?.nextBillingDate)
|
|
177
196
|
}
|
|
178
197
|
} catch (e) {
|
|
179
|
-
console.error(
|
|
198
|
+
console.error(e)
|
|
180
199
|
}
|
|
181
200
|
|
|
182
201
|
return {
|
|
183
|
-
_id: org._id
|
|
202
|
+
_id: `${org._id}-${page.value}`,
|
|
184
203
|
...org,
|
|
185
204
|
sites: `${sitesCount} Sites`,
|
|
186
205
|
planType,
|
|
187
206
|
billingCycle,
|
|
188
207
|
subscriptionStart,
|
|
189
208
|
subscriptionEnd,
|
|
190
|
-
status: org?.status || '
|
|
209
|
+
status: org?.status || 'active',
|
|
191
210
|
}
|
|
192
211
|
})
|
|
193
212
|
)
|
|
194
213
|
|
|
195
|
-
items.value =
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
items.value = []
|
|
215
|
+
|
|
216
|
+
await nextTick()
|
|
217
|
+
|
|
218
|
+
items.value = [...mapped]
|
|
219
|
+
|
|
220
|
+
pages.value = res?.pages || 1
|
|
221
|
+
|
|
222
|
+
const match = res?.pageRange?.match(/of\s+(\d+)/i)
|
|
223
|
+
|
|
224
|
+
totalItems.value = match ? Number(match[1]) : mapped.length
|
|
198
225
|
}
|
|
199
226
|
|
|
200
227
|
async function fetchSuspended() {
|
|
@@ -247,9 +274,13 @@ async function fetchSuspended() {
|
|
|
247
274
|
})
|
|
248
275
|
)
|
|
249
276
|
|
|
250
|
-
items.value = mapped
|
|
251
|
-
|
|
252
|
-
|
|
277
|
+
items.value = [...mapped]
|
|
278
|
+
|
|
279
|
+
pages.value = res?.pages || 1
|
|
280
|
+
|
|
281
|
+
const match = res?.pageRange?.match(/of\s+(\d+)/i)
|
|
282
|
+
|
|
283
|
+
totalItems.value = match ? Number(match[1]) : mapped.length
|
|
253
284
|
}
|
|
254
285
|
|
|
255
286
|
async function fetchPending() {
|
|
@@ -258,60 +289,75 @@ async function fetchPending() {
|
|
|
258
289
|
type: 'user-invite',
|
|
259
290
|
page: page.value,
|
|
260
291
|
search: search.value,
|
|
261
|
-
// app: 'organization',
|
|
262
292
|
})
|
|
263
293
|
|
|
264
|
-
|
|
294
|
+
items.value = [
|
|
295
|
+
...(await Promise.all(
|
|
265
296
|
(res?.items || []).map(async (i: any) => ({
|
|
266
297
|
...i,
|
|
267
298
|
app:
|
|
268
299
|
APP_LIST.find(a => a.value === i.metadata?.app)?.title ||
|
|
269
300
|
i.metadata?.app ||
|
|
270
301
|
'-',
|
|
302
|
+
|
|
271
303
|
role: await resolveRoleName(i.metadata?.role),
|
|
272
304
|
}))
|
|
273
|
-
)
|
|
305
|
+
))
|
|
306
|
+
]
|
|
274
307
|
|
|
275
308
|
pages.value = res?.pages || 1
|
|
276
|
-
|
|
309
|
+
|
|
310
|
+
const match = res?.pageRange?.match(/of\s+(\d+)/i)
|
|
311
|
+
|
|
312
|
+
totalItems.value = match
|
|
313
|
+
? Number(match[1])
|
|
314
|
+
: items.value.length
|
|
277
315
|
}
|
|
278
316
|
|
|
279
317
|
async function fetchData() {
|
|
280
318
|
loading.value = true
|
|
319
|
+
|
|
281
320
|
try {
|
|
282
|
-
if (tab.value === '
|
|
321
|
+
if (tab.value === 'active') {
|
|
322
|
+
await fetchActive()
|
|
323
|
+
}
|
|
324
|
+
else if (tab.value === 'suspended') {
|
|
283
325
|
items.value = []
|
|
284
326
|
pages.value = 1
|
|
285
327
|
totalItems.value = 0
|
|
286
|
-
return
|
|
287
328
|
}
|
|
288
|
-
|
|
289
|
-
|
|
329
|
+
else {
|
|
330
|
+
await fetchPending()
|
|
331
|
+
}
|
|
290
332
|
} finally {
|
|
291
333
|
loading.value = false
|
|
292
334
|
}
|
|
293
335
|
}
|
|
294
336
|
|
|
295
337
|
/* ================= EVENTS ================= */
|
|
296
|
-
function
|
|
297
|
-
page.value = 1
|
|
298
|
-
fetchData()
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function handleUpdatePage(p: number) {
|
|
338
|
+
async function handleUpdatePage(p: number) {
|
|
302
339
|
page.value = p
|
|
303
|
-
fetchData()
|
|
340
|
+
await fetchData()
|
|
341
|
+
}
|
|
342
|
+
async function onTabChange() {
|
|
343
|
+
page.value = 1
|
|
344
|
+
await fetchData()
|
|
304
345
|
}
|
|
305
346
|
|
|
306
347
|
function handleRefresh() {
|
|
307
348
|
fetchData()
|
|
308
349
|
}
|
|
309
350
|
|
|
351
|
+
let searchTimeout: any = null
|
|
352
|
+
|
|
310
353
|
function handleSearch() {
|
|
311
|
-
|
|
312
|
-
fetchData()
|
|
313
|
-
}
|
|
354
|
+
clearTimeout(searchTimeout)
|
|
314
355
|
|
|
356
|
+
searchTimeout = setTimeout(async () => {
|
|
357
|
+
page.value = 1
|
|
358
|
+
await fetchData()
|
|
359
|
+
}, 500)
|
|
360
|
+
}
|
|
315
361
|
/* ================= DIALOG ================= */
|
|
316
362
|
function openInviteDialog() {
|
|
317
363
|
dialog.value = true
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
</v-row>
|
|
25
25
|
</v-col>
|
|
26
26
|
|
|
27
|
-
<v-col
|
|
27
|
+
<v-col
|
|
28
|
+
v-if="!props.hideApp && props.adminState && APP === 'organization'"
|
|
29
|
+
cols="12"
|
|
30
|
+
class="mt-2"
|
|
31
|
+
>
|
|
28
32
|
<v-row no-gutters>
|
|
29
33
|
<InputLabel class="text-capitalize" title="App" required />
|
|
30
34
|
<v-col cols="12">
|
|
@@ -55,7 +59,7 @@
|
|
|
55
59
|
</v-row>
|
|
56
60
|
</v-col>
|
|
57
61
|
|
|
58
|
-
<v-col v-if="hasSite" cols="12">
|
|
62
|
+
<v-col v-if="hasSite && props.adminState" cols="12">
|
|
59
63
|
<v-row no-gutters>
|
|
60
64
|
<InputLabel class="text-capitalize" title="Site" />
|
|
61
65
|
<v-col cols="12">
|
|
@@ -139,6 +143,8 @@ import useUser from '../composables/useUser';
|
|
|
139
143
|
import useUtils from '../composables/useUtils';
|
|
140
144
|
|
|
141
145
|
const APP = useRuntimeConfig().public.APP;
|
|
146
|
+
|
|
147
|
+
|
|
142
148
|
const props = defineProps({
|
|
143
149
|
title: {
|
|
144
150
|
type: String,
|
|
@@ -177,13 +183,21 @@ const props = defineProps({
|
|
|
177
183
|
metadata: { app: "organization", role: "" },
|
|
178
184
|
}),
|
|
179
185
|
},
|
|
186
|
+
adminState: {
|
|
187
|
+
type: Boolean,
|
|
188
|
+
default: true
|
|
189
|
+
},
|
|
190
|
+
hideApp: {
|
|
191
|
+
type: Boolean,
|
|
192
|
+
default: false
|
|
193
|
+
}
|
|
180
194
|
});
|
|
181
195
|
|
|
182
196
|
const emit = defineEmits(["cancel", "success", "success:create-more"]);
|
|
183
197
|
|
|
184
198
|
const validForm = ref(false);
|
|
185
199
|
const form = ref<HTMLFormElement | null>(null);
|
|
186
|
-
const app = computed(() => useRuntimeConfig().public.APP ?? "");
|
|
200
|
+
// const app = computed(() => useRuntimeConfig().public.APP ?? "");
|
|
187
201
|
|
|
188
202
|
const loading = reactive({
|
|
189
203
|
submittingForm: false,
|
|
@@ -200,8 +214,24 @@ const invite = ref<Record<string, any>>({
|
|
|
200
214
|
isMemberInvite: false,
|
|
201
215
|
});
|
|
202
216
|
|
|
203
|
-
|
|
204
|
-
|
|
217
|
+
watch(
|
|
218
|
+
() => [props.hideApp, props.adminState, props.app, props.org],
|
|
219
|
+
() => {
|
|
220
|
+
invite.value.org = props.org || ""
|
|
221
|
+
|
|
222
|
+
if (props.hideApp) {
|
|
223
|
+
invite.value.app = "property_management_agency"
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
invite.value.app = props.adminState
|
|
228
|
+
? (props.app || "")
|
|
229
|
+
: "property_management_agency"
|
|
230
|
+
},
|
|
231
|
+
{ immediate: true }
|
|
232
|
+
)
|
|
233
|
+
// invite.value.app = props.app;
|
|
234
|
+
// invite.value.org = props.org ?? "";
|
|
205
235
|
|
|
206
236
|
if (props.mode === "edit") {
|
|
207
237
|
invite.value.email = props.invite.email;
|
|
@@ -1,45 +1,68 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<v-navigation-drawer
|
|
2
|
+
<v-navigation-drawer
|
|
3
|
+
v-model="drawer"
|
|
4
|
+
permanent
|
|
5
|
+
floating
|
|
6
|
+
class="bg-primary"
|
|
7
|
+
>
|
|
3
8
|
<v-list>
|
|
4
9
|
<v-list-item>
|
|
5
10
|
<v-list-item-title class="text-h6 text-white">
|
|
6
11
|
{{ props.title || APP_NAME }}
|
|
7
12
|
</v-list-item-title>
|
|
8
13
|
</v-list-item>
|
|
14
|
+
|
|
9
15
|
<slot name="action" />
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
|
|
17
|
+
<!-- NAVIGATION ITEMS -->
|
|
18
|
+
<v-tooltip
|
|
19
|
+
v-for="(item, index) in props.navigationItems"
|
|
20
|
+
:key="`${item.title}-${index}`"
|
|
21
|
+
location="right"
|
|
13
22
|
>
|
|
14
|
-
<
|
|
15
|
-
v-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
<template #activator="{ props: tooltipProps }">
|
|
24
|
+
<div v-bind="tooltipProps">
|
|
25
|
+
<NavigationItem
|
|
26
|
+
:title="item.title"
|
|
27
|
+
:icon="item.icon"
|
|
28
|
+
:route="item.route"
|
|
29
|
+
:link="item.link"
|
|
30
|
+
:class="{
|
|
31
|
+
'opacity-50 pointer-events-none': item.disabled
|
|
32
|
+
}"
|
|
33
|
+
@click="() => handleClick(item)"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<!-- TOOLTIP ONLY WHEN DISABLED -->
|
|
39
|
+
<span v-if="item.disabled">
|
|
40
|
+
Please complete onboarding first
|
|
41
|
+
</span>
|
|
42
|
+
</v-tooltip>
|
|
43
|
+
|
|
44
|
+
<!-- SERVICES SLOT -->
|
|
45
|
+
<slot name="services" />
|
|
32
46
|
</v-list>
|
|
33
47
|
</v-navigation-drawer>
|
|
34
48
|
</template>
|
|
35
49
|
|
|
36
50
|
<script setup lang="ts">
|
|
37
51
|
const props = defineProps({
|
|
38
|
-
navigationItems: { type: Array
|
|
52
|
+
navigationItems: { type: Array, required: true },
|
|
39
53
|
title: { type: String, default: "" }
|
|
40
|
-
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits(['navigate'])
|
|
41
57
|
|
|
42
|
-
const { drawer } = useLocal()
|
|
58
|
+
const { drawer } = useLocal()
|
|
59
|
+
const { APP_NAME } = useRuntimeConfig().public
|
|
43
60
|
|
|
44
|
-
|
|
61
|
+
/**
|
|
62
|
+
* GLOBAL CLICK CONTROL
|
|
63
|
+
*/
|
|
64
|
+
const handleClick = (item: any) => {
|
|
65
|
+
if (item.disabled) return
|
|
66
|
+
emit('navigate', item)
|
|
67
|
+
}
|
|
45
68
|
</script>
|
|
@@ -26,17 +26,11 @@
|
|
|
26
26
|
</v-row>
|
|
27
27
|
</v-col>
|
|
28
28
|
|
|
29
|
-
<v-col cols="12" class="mt-2">
|
|
29
|
+
<v-col v-if="props.siteState" cols="12" class="mt-2">
|
|
30
30
|
<v-row no-gutters>
|
|
31
31
|
<InputLabel title="Site" />
|
|
32
32
|
<v-col cols="12">
|
|
33
|
-
<v-text-field
|
|
34
|
-
:model-value="siteName"
|
|
35
|
-
density="comfortable"
|
|
36
|
-
readonly
|
|
37
|
-
disabled
|
|
38
|
-
:loading="loadSite"
|
|
39
|
-
/>
|
|
33
|
+
<v-text-field :model-value="siteName" density="comfortable" readonly disabled :loading="loadSite" />
|
|
40
34
|
</v-col>
|
|
41
35
|
</v-row>
|
|
42
36
|
</v-col>
|
|
@@ -53,12 +47,21 @@
|
|
|
53
47
|
</v-radio-group>
|
|
54
48
|
</v-col>
|
|
55
49
|
|
|
50
|
+
<v-col cols="12" class="mt-2">
|
|
51
|
+
<v-btn
|
|
52
|
+
size="small"
|
|
53
|
+
variant="tonal"
|
|
54
|
+
class="text-none mb-2"
|
|
55
|
+
@click="() => toggleSelectAll(!isAllSelected)"
|
|
56
|
+
>
|
|
57
|
+
{{ isAllSelected ? 'Unselect All' : 'Select All' }}
|
|
58
|
+
</v-btn>
|
|
59
|
+
</v-col>
|
|
60
|
+
|
|
56
61
|
<v-col cols="12" class="mt-3">
|
|
57
62
|
<v-card variant="outlined" border="thin" rounded="lg">
|
|
58
63
|
<template
|
|
59
|
-
v-for="(
|
|
60
|
-
actions, resourceKey, resourceIndex
|
|
61
|
-
) in props.permissions"
|
|
64
|
+
v-for="(actions, resourceKey, resourceIndex) in safePermissions"
|
|
62
65
|
:key="resourceKey"
|
|
63
66
|
>
|
|
64
67
|
<v-divider v-if="resourceIndex > 0" />
|
|
@@ -94,18 +97,14 @@
|
|
|
94
97
|
<!-- Expanded checkboxes when toggled on -->
|
|
95
98
|
<template v-if="isCategoryEnabled(String(resourceKey))">
|
|
96
99
|
<template
|
|
97
|
-
v-for="(
|
|
100
|
+
v-for="([actionKey, action]) in Object.entries(actions || {})"
|
|
98
101
|
:key="actionKey"
|
|
99
102
|
>
|
|
100
103
|
<v-divider />
|
|
101
104
|
<v-list-item density="compact" :ripple="false">
|
|
102
105
|
<template #prepend>
|
|
103
106
|
<v-checkbox-btn
|
|
104
|
-
:model-value="
|
|
105
|
-
selectedPermissions.includes(
|
|
106
|
-
`${String(resourceKey)}:${String(actionKey)}`
|
|
107
|
-
)
|
|
108
|
-
"
|
|
107
|
+
:model-value="permState.selected.includes(`${String(resourceKey)}:${String(actionKey)}`)"
|
|
109
108
|
@update:model-value="
|
|
110
109
|
toggleAction(
|
|
111
110
|
String(resourceKey),
|
|
@@ -204,59 +203,90 @@ const props = defineProps({
|
|
|
204
203
|
type: String,
|
|
205
204
|
default: "app",
|
|
206
205
|
},
|
|
206
|
+
siteState: {
|
|
207
|
+
type: Boolean,
|
|
208
|
+
default: true,
|
|
209
|
+
},
|
|
207
210
|
});
|
|
208
211
|
|
|
209
212
|
const emit = defineEmits(["cancel", "success", "success:create-more"]);
|
|
210
213
|
|
|
211
214
|
const validForm = ref(false);
|
|
212
215
|
const name = ref("");
|
|
213
|
-
const selectedPermissions = ref<Array<string>>([]);
|
|
214
216
|
const platform = ref("web");
|
|
215
217
|
const disable = ref(false);
|
|
216
218
|
const message = ref("");
|
|
217
219
|
|
|
218
|
-
const { requiredRule
|
|
220
|
+
const { requiredRule } = useUtils();
|
|
219
221
|
const { createRole } = useRole();
|
|
220
222
|
const { getSiteById } = useSite();
|
|
221
223
|
|
|
222
|
-
|
|
224
|
+
// Gom selected + enabled vào 1 shallowRef duy nhất
|
|
225
|
+
// để chỉ trigger 1 render cycle khi update cả 2
|
|
226
|
+
const permState = shallowRef({
|
|
227
|
+
selected: [] as string[],
|
|
228
|
+
enabled: [] as string[],
|
|
229
|
+
})
|
|
223
230
|
|
|
224
|
-
function
|
|
225
|
-
return
|
|
226
|
-
.length;
|
|
231
|
+
function isCategoryEnabled(resource: string) {
|
|
232
|
+
return permState.value.enabled.includes(resource)
|
|
227
233
|
}
|
|
228
234
|
|
|
229
|
-
function
|
|
230
|
-
return
|
|
235
|
+
function selectedActionCount(resource: string) {
|
|
236
|
+
return permState.value.selected.filter(p => p.startsWith(`${resource}:`)).length
|
|
231
237
|
}
|
|
232
238
|
|
|
233
239
|
function toggleCategory(resource: string, value: boolean | null) {
|
|
234
|
-
const
|
|
240
|
+
const { selected, enabled } = permState.value
|
|
235
241
|
if (value) {
|
|
236
|
-
|
|
242
|
+
if (!enabled.includes(resource)) {
|
|
243
|
+
permState.value = { selected, enabled: [...enabled, resource] }
|
|
244
|
+
}
|
|
237
245
|
} else {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
);
|
|
246
|
+
permState.value = {
|
|
247
|
+
selected: selected.filter(p => !p.startsWith(`${resource}:`)),
|
|
248
|
+
enabled: enabled.filter(r => r !== resource),
|
|
249
|
+
}
|
|
243
250
|
}
|
|
244
|
-
enabledCategories.value = updated;
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
function toggleAction(resource: string, action: string, value: boolean | null) {
|
|
248
|
-
|
|
254
|
+
if (value !== true && value !== false) return
|
|
255
|
+
const key = `${resource}:${action}`
|
|
256
|
+
const { selected, enabled } = permState.value
|
|
249
257
|
if (value) {
|
|
250
|
-
if (!
|
|
251
|
-
|
|
258
|
+
if (!selected.includes(key)) {
|
|
259
|
+
permState.value = { selected: [...selected, key], enabled }
|
|
252
260
|
}
|
|
253
261
|
} else {
|
|
254
|
-
|
|
255
|
-
(p) => p !== key
|
|
256
|
-
);
|
|
262
|
+
permState.value = { selected: selected.filter(p => p !== key), enabled }
|
|
257
263
|
}
|
|
258
264
|
}
|
|
259
265
|
|
|
266
|
+
function toggleSelectAll(value: boolean) {
|
|
267
|
+
if (!value) {
|
|
268
|
+
permState.value = { selected: [], enabled: [] }
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const perms = safePermissions.value
|
|
273
|
+
if (!perms) return
|
|
274
|
+
|
|
275
|
+
const allKeys: string[] = []
|
|
276
|
+
const categories: string[] = []
|
|
277
|
+
|
|
278
|
+
for (const [resource, actions] of Object.entries(perms)) {
|
|
279
|
+
if (!resource || !actions || typeof actions !== 'object') continue
|
|
280
|
+
categories.push(resource)
|
|
281
|
+
for (const action of Object.keys(actions)) {
|
|
282
|
+
if (action) allKeys.push(`${resource}:${action}`)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 1 assignment duy nhất = 1 render cycle, không crash
|
|
287
|
+
permState.value = { selected: allKeys, enabled: categories }
|
|
288
|
+
}
|
|
289
|
+
|
|
260
290
|
const siteName = ref("");
|
|
261
291
|
const loadSite = ref(false);
|
|
262
292
|
|
|
@@ -286,7 +316,7 @@ async function submit() {
|
|
|
286
316
|
const platformValue = platform.value === "web" ? "website" : platform.value;
|
|
287
317
|
await createRole({
|
|
288
318
|
name: name.value,
|
|
289
|
-
permissions:
|
|
319
|
+
permissions: permState.value.selected,
|
|
290
320
|
type: props.type,
|
|
291
321
|
org: props.org,
|
|
292
322
|
site: props.siteId,
|
|
@@ -301,11 +331,41 @@ async function submit() {
|
|
|
301
331
|
}
|
|
302
332
|
|
|
303
333
|
function cancel() {
|
|
304
|
-
name.value = ""
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
emit("cancel");
|
|
334
|
+
name.value = ""
|
|
335
|
+
permState.value = { selected: [], enabled: [] }
|
|
336
|
+
platform.value = "web"
|
|
337
|
+
message.value = ""
|
|
338
|
+
emit("cancel")
|
|
310
339
|
}
|
|
340
|
+
|
|
341
|
+
const safePermissions = computed(() => {
|
|
342
|
+
const p = props.permissions || {}
|
|
343
|
+
const cleaned: Record<string, any> = {}
|
|
344
|
+
|
|
345
|
+
for (const [k, v] of Object.entries(p)) {
|
|
346
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
347
|
+
cleaned[k] = v
|
|
348
|
+
} else {
|
|
349
|
+
cleaned[k] = {}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return cleaned
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const isAllSelected = computed(() => {
|
|
357
|
+
const perms = safePermissions.value
|
|
358
|
+
const allKeys: string[] = []
|
|
359
|
+
|
|
360
|
+
Object.entries(perms).forEach(([resource, actions]) => {
|
|
361
|
+
if (!actions) return
|
|
362
|
+
Object.keys(actions).forEach((action) => {
|
|
363
|
+
if (action) allKeys.push(`${resource}:${action}`)
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
return allKeys.length > 0 &&
|
|
368
|
+
allKeys.every(k => permState.value.selected.includes(k))
|
|
369
|
+
})
|
|
370
|
+
|
|
311
371
|
</script>
|
|
@@ -156,6 +156,73 @@
|
|
|
156
156
|
</v-col>
|
|
157
157
|
</v-row>
|
|
158
158
|
|
|
159
|
+
<v-row
|
|
160
|
+
v-if="selectedTask.areas && selectedTask.areas.length > 0"
|
|
161
|
+
no-gutters
|
|
162
|
+
class="mb-2"
|
|
163
|
+
>
|
|
164
|
+
<v-col cols="12">
|
|
165
|
+
<div class="text-subtitle-2 font-weight-bold mb-2">Units Under Selected Areas:</div>
|
|
166
|
+
<v-card variant="outlined" rounded>
|
|
167
|
+
<v-list density="compact" nav>
|
|
168
|
+
<template
|
|
169
|
+
v-for="(area, idx) in selectedTask.areas"
|
|
170
|
+
:key="area.value || area._id"
|
|
171
|
+
>
|
|
172
|
+
<v-divider v-if="Number(idx) > 0" />
|
|
173
|
+
<v-list-item
|
|
174
|
+
:title="area.name"
|
|
175
|
+
class="font-weight-medium"
|
|
176
|
+
@click="toggleViewArea(area.value || area._id)"
|
|
177
|
+
>
|
|
178
|
+
<template #prepend>
|
|
179
|
+
<v-icon size="16">
|
|
180
|
+
{{
|
|
181
|
+
openViewAreas.includes(area.value || area._id)
|
|
182
|
+
? 'mdi-chevron-down'
|
|
183
|
+
: 'mdi-chevron-right'
|
|
184
|
+
}}
|
|
185
|
+
</v-icon>
|
|
186
|
+
</template>
|
|
187
|
+
<template #append>
|
|
188
|
+
<v-progress-circular
|
|
189
|
+
v-if="loadingViewUnits && !(area.value || area._id in fetchedViewAreaUnits)"
|
|
190
|
+
size="14"
|
|
191
|
+
width="2"
|
|
192
|
+
indeterminate
|
|
193
|
+
color="primary"
|
|
194
|
+
/>
|
|
195
|
+
<v-chip v-else size="x-small" variant="tonal" color="primary">
|
|
196
|
+
{{ (fetchedViewAreaUnits[area.value || area._id] || []).length }}
|
|
197
|
+
</v-chip>
|
|
198
|
+
</template>
|
|
199
|
+
</v-list-item>
|
|
200
|
+
<v-expand-transition>
|
|
201
|
+
<div v-show="openViewAreas.includes(area.value || area._id)">
|
|
202
|
+
<v-card-text class="px-3 py-2">
|
|
203
|
+
<div
|
|
204
|
+
v-if="(fetchedViewAreaUnits[area.value || area._id] || []).length > 0"
|
|
205
|
+
class="d-flex flex-wrap ga-3"
|
|
206
|
+
>
|
|
207
|
+
<v-chip
|
|
208
|
+
v-for="unit in fetchedViewAreaUnits[area.value || area._id]"
|
|
209
|
+
:key="unit.unit"
|
|
210
|
+
size="small"
|
|
211
|
+
variant="tonal"
|
|
212
|
+
>
|
|
213
|
+
{{ unit.name }}
|
|
214
|
+
</v-chip>
|
|
215
|
+
</div>
|
|
216
|
+
<span v-else class="text-caption text-disabled">No units configured</span>
|
|
217
|
+
</v-card-text>
|
|
218
|
+
</div>
|
|
219
|
+
</v-expand-transition>
|
|
220
|
+
</template>
|
|
221
|
+
</v-list>
|
|
222
|
+
</v-card>
|
|
223
|
+
</v-col>
|
|
224
|
+
</v-row>
|
|
225
|
+
|
|
159
226
|
<v-row v-if="selectedTask.description" no-gutters class="mb-2">
|
|
160
227
|
<v-col cols="5" class="text-subtitle-2 font-weight-bold">
|
|
161
228
|
Description:
|
|
@@ -260,6 +327,7 @@
|
|
|
260
327
|
<script lang="ts" setup>
|
|
261
328
|
import useScheduleTask from "../composables/useScheduleTask";
|
|
262
329
|
import useUtils from "../composables/useUtils";
|
|
330
|
+
import useAreas from "../composables/useAreas";
|
|
263
331
|
|
|
264
332
|
const props = defineProps({
|
|
265
333
|
orgId: { type: String, required: true },
|
|
@@ -276,6 +344,7 @@ const props = defineProps({
|
|
|
276
344
|
const { formatDate, debounce } = useUtils();
|
|
277
345
|
const { getScheduleTasks, getScheduleTaskById, deleteScheduleTask } =
|
|
278
346
|
useScheduleTask();
|
|
347
|
+
const { getAreaById } = useAreas();
|
|
279
348
|
|
|
280
349
|
const canViewScheduleTasks = computed(() => props.canViewScheduleTasks);
|
|
281
350
|
const canCreateScheduleTask = computed(() => props.canCreateScheduleTask);
|
|
@@ -301,6 +370,42 @@ const message = ref("");
|
|
|
301
370
|
const messageSnackbar = ref(false);
|
|
302
371
|
const messageColor = ref("success");
|
|
303
372
|
|
|
373
|
+
const loadingViewUnits = ref(false);
|
|
374
|
+
const fetchedViewAreaUnits = ref<Record<string, { name: string; unit: string }[]>>({});
|
|
375
|
+
const openViewAreas = ref<string[]>([]);
|
|
376
|
+
|
|
377
|
+
const toggleViewArea = (id: string) => {
|
|
378
|
+
const idx = openViewAreas.value.indexOf(id);
|
|
379
|
+
if (idx === -1) openViewAreas.value.push(id);
|
|
380
|
+
else openViewAreas.value.splice(idx, 1);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
watch(dialogShowMoreActions, async (isOpen) => {
|
|
384
|
+
if (!isOpen) return;
|
|
385
|
+
const areas = (selectedTask.value?.areas || []) as any[];
|
|
386
|
+
if (areas.length === 0) return;
|
|
387
|
+
openViewAreas.value = areas.map((a) => a.value || a._id);
|
|
388
|
+
const toFetch = areas
|
|
389
|
+
.map((a) => a.value || a._id)
|
|
390
|
+
.filter((id) => id && !(id in fetchedViewAreaUnits.value));
|
|
391
|
+
if (toFetch.length === 0) return;
|
|
392
|
+
loadingViewUnits.value = true;
|
|
393
|
+
try {
|
|
394
|
+
await Promise.all(
|
|
395
|
+
toFetch.map(async (id: string) => {
|
|
396
|
+
try {
|
|
397
|
+
const result = await getAreaById(id);
|
|
398
|
+
fetchedViewAreaUnits.value[id] = result?.units || [];
|
|
399
|
+
} catch {
|
|
400
|
+
fetchedViewAreaUnits.value[id] = [];
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
);
|
|
404
|
+
} finally {
|
|
405
|
+
loadingViewUnits.value = false;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
304
409
|
const items = ref<any[]>([]);
|
|
305
410
|
|
|
306
411
|
const headers = [
|
|
@@ -775,8 +775,12 @@ onMounted(() => {
|
|
|
775
775
|
vehicle.seasonPassType = prop.vehicleData.seasonPassType || '';
|
|
776
776
|
vehicle.end = prop.vehicleData.end || '';
|
|
777
777
|
vehicle.start = prop.vehicleData.start || '';
|
|
778
|
-
vehicle.plateNumber = prop.vehicleData.
|
|
778
|
+
vehicle.plateNumber = prop.vehicleData.plateNumber
|
|
779
779
|
vehicle.peopleId = prop.vehicleData.peopleId || '';
|
|
780
|
+
|
|
781
|
+
if(prop.type === "seasonpass"){
|
|
782
|
+
showSubscriptionDateOptions.value = true;
|
|
783
|
+
}
|
|
780
784
|
}
|
|
781
785
|
}, 300);
|
|
782
786
|
})
|
|
@@ -37,13 +37,15 @@
|
|
|
37
37
|
</v-dialog>
|
|
38
38
|
|
|
39
39
|
<v-dialog v-model="dialog.createVehicle" v-if="vehicleType" width="450" persistent>
|
|
40
|
-
<VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedVehicleObject"
|
|
41
|
-
|
|
40
|
+
<VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedVehicleObject"
|
|
41
|
+
:plate-number-id="selectedPlateNumberId" @back="handleBackToSelection" @done="handleAddVehicleComplete"
|
|
42
|
+
:org="org" :site="props.site" @close:all="handleCloseAll" />
|
|
42
43
|
</v-dialog>
|
|
43
44
|
|
|
44
45
|
<v-dialog v-model="dialog.updateVehicle" v-if="vehicleType" width="450" persistent>
|
|
45
|
-
<VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedPlateNumberObject"
|
|
46
|
-
|
|
46
|
+
<VehicleForm :type="vehicleType" :mode="mode" :vehicle-data="selectedPlateNumberObject"
|
|
47
|
+
:plate-number-id="selectedPlateNumberId" @back="handleBackToSelection" @done="handleAddVehicleComplete"
|
|
48
|
+
:org="org" :site="props.site" @close:all="handleCloseAll" @close="handleCloseAll" />
|
|
47
49
|
</v-dialog>
|
|
48
50
|
|
|
49
51
|
<!-- <v-dialog v-model="dialog.updateVehicle" v-if="vehicleType" width="450" persistent>
|
|
@@ -77,7 +79,8 @@
|
|
|
77
79
|
</template>
|
|
78
80
|
<v-list density="compact">
|
|
79
81
|
<v-list-item v-if="(plateNumberItem as TPlateNumber)?.status == 'active'"
|
|
80
|
-
title="Edit Vehicle" prepend-icon="mdi-pencil"
|
|
82
|
+
title="Edit Vehicle" prepend-icon="mdi-pencil"
|
|
83
|
+
@click="handleEditVehicleAction(plateNumberItem as TPlateNumber)" />
|
|
81
84
|
<v-list-item
|
|
82
85
|
v-if="props.app === 'property_management_agency' && (plateNumberItem as TPlateNumber)?.status == 'pending'"
|
|
83
86
|
title="Approve" prepend-icon="mdi-check-circle" base-color="success"
|
|
@@ -110,7 +113,7 @@
|
|
|
110
113
|
<v-col v-else-if="selectedVehicleObject[key]" cols="12">
|
|
111
114
|
<span class="d-flex ga-3 align-center"><strong>{{ label }}:</strong> {{ formatValues(key,
|
|
112
115
|
selectedVehicleObject[key])
|
|
113
|
-
|
|
116
|
+
}}</span>
|
|
114
117
|
</v-col>
|
|
115
118
|
</template>
|
|
116
119
|
</v-row>
|
|
@@ -184,7 +187,7 @@ const plateHeaders = [
|
|
|
184
187
|
|
|
185
188
|
|
|
186
189
|
const { formatCamelCaseToWords, formatDate, debounce } = useUtils();
|
|
187
|
-
const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, updateVehicle,
|
|
190
|
+
const { getVehicles, deleteVehicle, formatVehicleStatus, approveVehicle, updateVehicle, getSpecificVehicleById } = useVehicle();
|
|
188
191
|
|
|
189
192
|
const items = ref<Array<Record<string, any>>>([]);
|
|
190
193
|
const page = ref(1);
|
|
@@ -322,23 +325,30 @@ async function handleEditVehicleAction(item: TPlateNumber) {
|
|
|
322
325
|
|
|
323
326
|
try {
|
|
324
327
|
loadingPlateNumberDetails.value = true;
|
|
325
|
-
const res = await
|
|
326
|
-
|
|
328
|
+
const res = await getSpecificVehicleById(selectedPlateNumberId.value as string)
|
|
329
|
+
|
|
330
|
+
if (res.type === 'blocklist') {
|
|
331
|
+
vehicleType.value = "blocklist";
|
|
332
|
+
} else {
|
|
333
|
+
if (res?.seasonPassType) {
|
|
327
334
|
vehicleType.value = "seasonpass";
|
|
328
335
|
} else {
|
|
329
|
-
vehicleType.value =
|
|
336
|
+
vehicleType.value = "whitelist";
|
|
330
337
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
selectedPlateNumberObject.value = res || null;
|
|
342
|
+
dialog.showMoreActions = false;
|
|
343
|
+
mode.value = "edit";
|
|
344
|
+
dialog.updateVehicle = true;
|
|
345
|
+
|
|
336
346
|
} catch (error) {
|
|
337
347
|
console.error("Error fetching vehicle details:", error);
|
|
338
348
|
} finally {
|
|
339
349
|
loadingPlateNumberDetails.value = false;
|
|
340
|
-
}
|
|
341
|
-
|
|
350
|
+
}
|
|
351
|
+
|
|
342
352
|
}
|
|
343
353
|
|
|
344
354
|
function handleDeleteVehicleAction(item: TPlateNumber) {
|
|
@@ -131,6 +131,18 @@ export default function useVehicle() {
|
|
|
131
131
|
return { label, color }
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// fetch specific plateNumber Vehicle data
|
|
135
|
+
|
|
136
|
+
function getSpecificVehicleById(id: string){
|
|
137
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
138
|
+
`/api/vehicles/specific/${id}`,
|
|
139
|
+
{
|
|
140
|
+
method: "GET",
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
134
146
|
return {
|
|
135
147
|
getVehicles,
|
|
136
148
|
addVehicle,
|
|
@@ -141,6 +153,7 @@ export default function useVehicle() {
|
|
|
141
153
|
getVehicleByNRIC,
|
|
142
154
|
formatVehicleStatus,
|
|
143
155
|
approveVehicle,
|
|
144
|
-
searchMultipleVehiclesByUnitId
|
|
156
|
+
searchMultipleVehiclesByUnitId,
|
|
157
|
+
getSpecificVehicleById
|
|
145
158
|
};
|
|
146
159
|
}
|
package/package.json
CHANGED