@7365admin1/layer-common 1.11.45 → 1.11.47
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 +14 -0
- package/components/AccessCardPreviewDialog.vue +70 -13
- package/components/AccessManagement.vue +11 -0
- package/components/ClientMain.vue +70 -58
- package/components/DocumentForm.vue +48 -17
- package/components/DocumentManagement.vue +176 -32
- package/components/InvitationForm.vue +6 -2
- package/components/RolePermissionFormCreate.vue +2 -3
- package/components/TableV3.vue +10 -0
- package/composables/useAccessManagement.ts +16 -0
- package/composables/useDocument.ts +26 -4
- package/composables/useOrg.ts +60 -26
- package/composables/usePeople.ts +24 -5
- package/package.json +1 -1
- package/types/customer.d.ts +7 -1
- package/types/people.d.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 1.11.47
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3ca3d41: fix search, filter and add get orgs with subscription
|
|
8
|
+
- ecacc9d: update site category, get permissions by invitation
|
|
9
|
+
|
|
10
|
+
## 1.11.46
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 1e4f7b6: update latest version add prop for invitation form
|
|
15
|
+
- 2e14129: update latest layer common
|
|
16
|
+
|
|
3
17
|
## 1.11.45
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
+
<Snackbar v-model="snackbar" :text="snackbarMessage" :color="snackbarColor" />
|
|
3
|
+
|
|
2
4
|
<AccessCardDetailsDialog
|
|
3
5
|
v-model="historyDialog"
|
|
4
6
|
:card="selectedCardInUnit"
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
</div>
|
|
21
23
|
|
|
22
24
|
<!-- Person Type Filter -->
|
|
23
|
-
<div class="text-caption font-weight-bold mb-1 text-uppercase text-grey-darken-1">Person Type</div>
|
|
25
|
+
<!-- <div class="text-caption font-weight-bold mb-1 text-uppercase text-grey-darken-1">Person Type</div>
|
|
24
26
|
<v-btn-toggle
|
|
25
27
|
v-model="assignPersonType"
|
|
26
28
|
color="primary"
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
<v-btn value="owner" size="small" class="text-none px-3">Unit Owner</v-btn>
|
|
34
36
|
<v-btn value="resident" size="small" class="text-none px-3">Resident</v-btn>
|
|
35
37
|
<v-btn value="tenant" size="small" class="text-none px-3">Tenant</v-btn>
|
|
36
|
-
</v-btn-toggle>
|
|
38
|
+
</v-btn-toggle> -->
|
|
37
39
|
|
|
38
40
|
<!-- Person Selector -->
|
|
39
41
|
<div class="text-caption font-weight-bold mb-1 text-uppercase text-grey-darken-1">
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
</v-chip>
|
|
63
65
|
</template>
|
|
64
66
|
</v-autocomplete>
|
|
67
|
+
<p v-if="assignError" class="text-error text-caption mt-2">{{ assignError }}</p>
|
|
65
68
|
</v-card-text>
|
|
66
69
|
|
|
67
70
|
<v-toolbar density="compact">
|
|
@@ -84,7 +87,8 @@
|
|
|
84
87
|
color="black"
|
|
85
88
|
class="text-none"
|
|
86
89
|
height="48"
|
|
87
|
-
:disabled="!assignPerson"
|
|
90
|
+
:disabled="!assignPerson || assignLoading"
|
|
91
|
+
:loading="assignLoading"
|
|
88
92
|
@click="confirmAssign"
|
|
89
93
|
>
|
|
90
94
|
Assign
|
|
@@ -384,6 +388,10 @@ const props = defineProps({
|
|
|
384
388
|
type: String,
|
|
385
389
|
default: "",
|
|
386
390
|
},
|
|
391
|
+
orgId: {
|
|
392
|
+
type: String,
|
|
393
|
+
default: "",
|
|
394
|
+
},
|
|
387
395
|
});
|
|
388
396
|
|
|
389
397
|
const emit = defineEmits<{
|
|
@@ -391,10 +399,11 @@ const emit = defineEmits<{
|
|
|
391
399
|
"update:selectedCardInUnit": [value: Record<string, any> | null];
|
|
392
400
|
replace: [];
|
|
393
401
|
delete: [];
|
|
402
|
+
refresh: [];
|
|
394
403
|
"assign-to-person": [{ card: Record<string, any> | null; person: Record<string, any> | null }];
|
|
395
404
|
}>();
|
|
396
405
|
|
|
397
|
-
const {
|
|
406
|
+
const { getResidents, assignUser } = useAccessManagement();
|
|
398
407
|
|
|
399
408
|
const historyDialog = ref(false);
|
|
400
409
|
const showAssignForm = ref(false);
|
|
@@ -402,6 +411,18 @@ const assignPersonType = ref<string>("");
|
|
|
402
411
|
const assignPerson = ref<Record<string, any> | null>(null);
|
|
403
412
|
const peopleItems = ref<Record<string, any>[]>([]);
|
|
404
413
|
const peopleLoading = ref(false);
|
|
414
|
+
const assignLoading = ref(false);
|
|
415
|
+
const assignError = ref("");
|
|
416
|
+
const encryptedAcmUrl = ref("");
|
|
417
|
+
const snackbar = ref(false);
|
|
418
|
+
const snackbarMessage = ref("");
|
|
419
|
+
const snackbarColor = ref<"success" | "error">("success");
|
|
420
|
+
|
|
421
|
+
function showToast(message: string, color: "success" | "error") {
|
|
422
|
+
snackbarMessage.value = message;
|
|
423
|
+
snackbarColor.value = color;
|
|
424
|
+
snackbar.value = true;
|
|
425
|
+
}
|
|
405
426
|
|
|
406
427
|
const isSelectedCardAvailable = computed(() => {
|
|
407
428
|
if (!props.selectedCardInUnit?._id) return false;
|
|
@@ -440,13 +461,19 @@ async function openAssignForm() {
|
|
|
440
461
|
showAssignForm.value = true;
|
|
441
462
|
assignPerson.value = null;
|
|
442
463
|
assignPersonType.value = "";
|
|
464
|
+
assignError.value = "";
|
|
443
465
|
peopleItems.value = [];
|
|
466
|
+
encryptedAcmUrl.value = "";
|
|
444
467
|
|
|
445
|
-
if (!props.unit?._id) return;
|
|
468
|
+
if (!props.unit?._id || !props.orgId || !props.siteId) return;
|
|
446
469
|
peopleLoading.value = true;
|
|
447
470
|
try {
|
|
448
|
-
const
|
|
449
|
-
|
|
471
|
+
const [residentsRes, acmRes] = await Promise.all([
|
|
472
|
+
getResidents({ orgId: props.orgId, siteId: props.siteId, unitId: props.unit._id }),
|
|
473
|
+
$fetch<{ encrypted: string }>("/api/encrypt-acm-url"),
|
|
474
|
+
]);
|
|
475
|
+
peopleItems.value = residentsRes?.data ?? [];
|
|
476
|
+
encryptedAcmUrl.value = acmRes?.encrypted ?? "";
|
|
450
477
|
} catch {
|
|
451
478
|
peopleItems.value = [];
|
|
452
479
|
} finally {
|
|
@@ -458,14 +485,44 @@ function cancelAssign() {
|
|
|
458
485
|
showAssignForm.value = false;
|
|
459
486
|
assignPerson.value = null;
|
|
460
487
|
assignPersonType.value = "";
|
|
488
|
+
assignError.value = "";
|
|
461
489
|
}
|
|
462
490
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
491
|
+
const selectedCardType = computed(() => {
|
|
492
|
+
const id = props.selectedCardInUnit?._id;
|
|
493
|
+
if (!id) return "QRCODE";
|
|
494
|
+
const isPhysical = [
|
|
495
|
+
...(props.unit?.available?.physical ?? []),
|
|
496
|
+
...(props.unit?.assigned?.physical ?? []),
|
|
497
|
+
].some((c: any) => c._id === id);
|
|
498
|
+
return isPhysical ? "NFC" : "QRCODE";
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
async function confirmAssign() {
|
|
502
|
+
if (!assignPerson.value || !props.unit?._id) return;
|
|
503
|
+
assignLoading.value = true;
|
|
504
|
+
assignError.value = "";
|
|
505
|
+
try {
|
|
506
|
+
await assignUser({
|
|
507
|
+
assignees: [assignPerson.value._id],
|
|
508
|
+
unit: props.unit._id,
|
|
509
|
+
type: selectedCardType.value,
|
|
510
|
+
acm_url: encryptedAcmUrl.value,
|
|
511
|
+
});
|
|
512
|
+
emit("assign-to-person", {
|
|
513
|
+
card: props.selectedCardInUnit,
|
|
514
|
+
person: assignPerson.value,
|
|
515
|
+
});
|
|
516
|
+
showToast("Card assigned successfully.", "success");
|
|
517
|
+
cancelAssign();
|
|
518
|
+
emit("refresh");
|
|
519
|
+
} catch (e: any) {
|
|
520
|
+
const msg = e?.data?.message ?? "Failed to assign. Please try again.";
|
|
521
|
+
assignError.value = msg;
|
|
522
|
+
showToast(msg, "error");
|
|
523
|
+
} finally {
|
|
524
|
+
assignLoading.value = false;
|
|
525
|
+
}
|
|
469
526
|
}
|
|
470
527
|
|
|
471
528
|
function toggleCard(card: Record<string, any>) {
|
|
@@ -113,8 +113,10 @@
|
|
|
113
113
|
:is-selected-card-assigned-physical="isSelectedCardAssignedPhysical"
|
|
114
114
|
:is-selected-card-physical="isSelectedCardPhysical"
|
|
115
115
|
:site-id="siteId"
|
|
116
|
+
:org-id="orgId"
|
|
116
117
|
@replace="openReplaceDialog()"
|
|
117
118
|
@delete="openDeleteDialog()"
|
|
119
|
+
@refresh="handleAssignRefresh"
|
|
118
120
|
/>
|
|
119
121
|
|
|
120
122
|
<!-- Replace Card Dialog -->
|
|
@@ -304,6 +306,15 @@ function setCard({ mode = "create", dialog = true, data = {} as TCard } = {}) {
|
|
|
304
306
|
}
|
|
305
307
|
}
|
|
306
308
|
|
|
309
|
+
async function handleAssignRefresh() {
|
|
310
|
+
await getCards();
|
|
311
|
+
await nextTick();
|
|
312
|
+
statsRef.value?.refresh();
|
|
313
|
+
const updated = items.value.find((item: any) => item._id === selectedCard.value?._id);
|
|
314
|
+
if (updated) selectedCard.value = updated;
|
|
315
|
+
previewDialog.value = true;
|
|
316
|
+
}
|
|
317
|
+
|
|
307
318
|
function successCreate() {
|
|
308
319
|
createDialog.value = false;
|
|
309
320
|
getCards();
|
|
@@ -228,7 +228,7 @@ import useSubscription from '../composables/useSubscription'
|
|
|
228
228
|
type TabStatus = 'active' | 'suspended' | 'pending'
|
|
229
229
|
|
|
230
230
|
/* ── COMPOSABLES ── */
|
|
231
|
-
const {
|
|
231
|
+
const { getOrganizationsWithSubscription } = useOrg()
|
|
232
232
|
const { getVerifications, cancelUserInvitation } = useVerification()
|
|
233
233
|
const { getCustomerSites: getCustomerSitesByOrgId } = useCustomerSite()
|
|
234
234
|
const { getByOrgId: getSubscriptionByOrgId } = useSubscription()
|
|
@@ -250,7 +250,7 @@ const APP_LIST = [
|
|
|
250
250
|
const tab = ref<TabStatus>('active')
|
|
251
251
|
const dialog = ref(false)
|
|
252
252
|
const openMenuId = ref<string | null>(null)
|
|
253
|
-
// filterStatus
|
|
253
|
+
// filterStatus
|
|
254
254
|
const filterStatus = ref<'active' | 'suspended'>('active')
|
|
255
255
|
const filterPlan = ref('') // '' = All | any APP_LIST value
|
|
256
256
|
const filterBilling = ref('') // '' = All | 'monthly' | 'yearly'
|
|
@@ -303,60 +303,72 @@ function parseTotalItems(pageRangeStr: string, fallback: number): number {
|
|
|
303
303
|
return match ? Number(match[1]) : fallback
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
/* ── FETCH ── */
|
|
307
|
-
async function mapOrg(org: any, status: string) {
|
|
308
|
-
let sitesCount = 0, planType = '-', billingCycle = '-',
|
|
309
|
-
subscriptionStart = '-', subscriptionEnd = '-'
|
|
310
|
-
try {
|
|
311
|
-
const siteRes = await getCustomerSitesByOrgId(org._id)
|
|
312
|
-
sitesCount = siteRes?.data?.totalItems
|
|
313
|
-
?? siteRes?.totalItems
|
|
314
|
-
?? siteRes?.data?.items?.length
|
|
315
|
-
?? siteRes?.items?.length
|
|
316
|
-
?? 0
|
|
317
|
-
|
|
318
|
-
const subRes = await getSubscriptionByOrgId(org._id)
|
|
319
|
-
const sub = subRes?.data ?? subRes
|
|
320
|
-
if (sub) {
|
|
321
|
-
planType = planTypeLabel(sub)
|
|
322
|
-
billingCycle = sub?.billingCycle ?? '-'
|
|
323
|
-
subscriptionStart = formatDate(sub?.createdAt)
|
|
324
|
-
subscriptionEnd = formatDate(sub?.nextBillingDate)
|
|
325
|
-
}
|
|
326
|
-
} catch (e) {
|
|
327
|
-
console.error('mapOrg error:', org._id, e)
|
|
328
|
-
}
|
|
329
|
-
return {
|
|
330
|
-
_id: `${org._id}-${page.value}`,
|
|
331
|
-
...org,
|
|
332
|
-
sites: `${sitesCount} Sites`,
|
|
333
|
-
planType,
|
|
334
|
-
billingCycle,
|
|
335
|
-
subscriptionStart,
|
|
336
|
-
subscriptionEnd,
|
|
337
|
-
status: org?.status ?? status,
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
306
|
|
|
341
307
|
async function fetchOrgs(status: 'active' | 'suspended') {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
308
|
+
loading.value = true
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const res = await getOrganizationsWithSubscription({
|
|
312
|
+
page: page.value,
|
|
313
|
+
search: search.value,
|
|
314
|
+
limit: limit.value,
|
|
315
|
+
status,
|
|
316
|
+
type: filterPlan.value,
|
|
317
|
+
billingCycle: filterBilling.value,
|
|
318
|
+
})
|
|
351
319
|
|
|
352
|
-
|
|
353
|
-
const mapped = await Promise.all(orgs.map((org: any) => mapOrg(org, status)))
|
|
320
|
+
const orgs = res?.data?.items ?? res?.items ?? []
|
|
354
321
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
322
|
+
const siteCache: Record<string, number> = {}
|
|
323
|
+
|
|
324
|
+
items.value = await Promise.all(
|
|
325
|
+
orgs.map(async (org: any) => {
|
|
326
|
+
const sub = org?.subscription ?? {}
|
|
327
|
+
|
|
328
|
+
let siteCount = siteCache[org._id]
|
|
329
|
+
|
|
330
|
+
if (siteCount === undefined) {
|
|
331
|
+
const sitesRes = await getCustomerSitesByOrgId(org._id)
|
|
332
|
+
|
|
333
|
+
siteCount =
|
|
334
|
+
sitesRes?.items?.length ??
|
|
335
|
+
sitesRes?.data?.items?.length ??
|
|
336
|
+
0
|
|
337
|
+
|
|
338
|
+
siteCache[org._id] = siteCount
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
_id: org._id,
|
|
343
|
+
|
|
344
|
+
name: org.name,
|
|
345
|
+
email: org.email,
|
|
346
|
+
nature: org.nature,
|
|
347
|
+
|
|
348
|
+
sites: `${siteCount} Sites`,
|
|
349
|
+
|
|
350
|
+
planType: planTypeLabel(sub),
|
|
351
|
+
|
|
352
|
+
billingCycle: sub?.billingCycle ?? "-",
|
|
353
|
+
|
|
354
|
+
subscriptionStart: formatDate(sub?.createdAt),
|
|
355
|
+
|
|
356
|
+
subscriptionEnd: formatDate(sub?.nextBillingDate),
|
|
357
|
+
|
|
358
|
+
status: org?.status ?? status,
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
pages.value = res?.pages ?? 1
|
|
364
|
+
|
|
365
|
+
totalItems.value = parseTotalItems(
|
|
366
|
+
res?.pageRange,
|
|
367
|
+
items.value.length
|
|
368
|
+
)
|
|
369
|
+
} finally {
|
|
370
|
+
loading.value = false
|
|
371
|
+
}
|
|
360
372
|
}
|
|
361
373
|
|
|
362
374
|
async function fetchPending() {
|
|
@@ -364,7 +376,8 @@ async function fetchPending() {
|
|
|
364
376
|
status: 'pending',
|
|
365
377
|
type: 'user-invite',
|
|
366
378
|
page: page.value,
|
|
367
|
-
search: search.value,
|
|
379
|
+
search: search.value,
|
|
380
|
+
email: search.value,
|
|
368
381
|
})
|
|
369
382
|
|
|
370
383
|
items.value = await Promise.all(
|
|
@@ -391,7 +404,7 @@ async function fetchData() {
|
|
|
391
404
|
}
|
|
392
405
|
}
|
|
393
406
|
|
|
394
|
-
/* ── WATCH: filterStatus
|
|
407
|
+
/* ── WATCH: filterStatus ── */
|
|
395
408
|
watch(filterStatus, (val) => {
|
|
396
409
|
tab.value = val
|
|
397
410
|
page.value = 1
|
|
@@ -406,7 +419,7 @@ async function handleUpdatePage(p: number) {
|
|
|
406
419
|
|
|
407
420
|
async function onTabChange(value: TabStatus) {
|
|
408
421
|
tab.value = value
|
|
409
|
-
// sync filterStatus
|
|
422
|
+
// sync filterStatus ( active/suspended)
|
|
410
423
|
if (value !== 'pending') filterStatus.value = value
|
|
411
424
|
page.value = 1
|
|
412
425
|
await fetchData()
|
|
@@ -421,13 +434,12 @@ function handleSearch() {
|
|
|
421
434
|
if (searchTimeout) clearTimeout(searchTimeout)
|
|
422
435
|
searchTimeout = setTimeout(() => {
|
|
423
436
|
page.value = 1
|
|
424
|
-
fetchData()
|
|
437
|
+
fetchData()
|
|
425
438
|
}, 300)
|
|
426
439
|
}
|
|
427
440
|
|
|
428
441
|
function handleFilterChange() {
|
|
429
|
-
|
|
430
|
-
// nếu chỉ plan/billing thay đổi thì gọi thủ công
|
|
442
|
+
|
|
431
443
|
page.value = 1
|
|
432
444
|
fetchData()
|
|
433
445
|
}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
v-model="document.attachment"
|
|
16
16
|
:multiple="false"
|
|
17
17
|
:max-length="10"
|
|
18
|
-
title="Upload
|
|
19
|
-
accept=".pdf
|
|
18
|
+
title="Upload Files"
|
|
19
|
+
accept=".pdf,.csv,.doc,.docx"
|
|
20
20
|
/>
|
|
21
21
|
</v-col>
|
|
22
22
|
|
|
@@ -28,15 +28,6 @@
|
|
|
28
28
|
></v-text-field>
|
|
29
29
|
</v-col>
|
|
30
30
|
|
|
31
|
-
<v-col cols="12" class="px-6">
|
|
32
|
-
<InputLabel class="text-capitalize" title="Document Type" />
|
|
33
|
-
<v-select
|
|
34
|
-
v-model="document.type"
|
|
35
|
-
:items="documentTypes"
|
|
36
|
-
density="comfortable"
|
|
37
|
-
/>
|
|
38
|
-
</v-col>
|
|
39
|
-
|
|
40
31
|
<v-col cols="12">
|
|
41
32
|
<v-row no-gutters>
|
|
42
33
|
<v-col cols="12" class="text-center">
|
|
@@ -97,6 +88,10 @@ const prop = defineProps({
|
|
|
97
88
|
type: String,
|
|
98
89
|
default: "add",
|
|
99
90
|
},
|
|
91
|
+
parentId: {
|
|
92
|
+
type: String,
|
|
93
|
+
default: "",
|
|
94
|
+
},
|
|
100
95
|
document: {
|
|
101
96
|
type: Object as PropType<TDocument>,
|
|
102
97
|
default: () => ({
|
|
@@ -116,8 +111,6 @@ const validForm = ref(false);
|
|
|
116
111
|
const disable = ref(false);
|
|
117
112
|
const message = ref("");
|
|
118
113
|
|
|
119
|
-
const documentTypes = ref(["Online Form", "Other"]);
|
|
120
|
-
|
|
121
114
|
const document = ref<Record<string, any>>({
|
|
122
115
|
name: "",
|
|
123
116
|
attachment: [],
|
|
@@ -127,6 +120,10 @@ const document = ref<Record<string, any>>({
|
|
|
127
120
|
const route = useRoute();
|
|
128
121
|
const siteId = route.params.site as string;
|
|
129
122
|
const orgId = route.params.org as string;
|
|
123
|
+
const routeParentId = computed(() =>
|
|
124
|
+
typeof route.query.parentId === "string" ? route.query.parentId : ""
|
|
125
|
+
);
|
|
126
|
+
const effectiveParentId = computed(() => prop.parentId || routeParentId.value);
|
|
130
127
|
|
|
131
128
|
if (prop.mode === "edit") {
|
|
132
129
|
document.value.name = prop.document.name;
|
|
@@ -138,7 +135,8 @@ watch(
|
|
|
138
135
|
() => document.value.attachment,
|
|
139
136
|
async (newVal) => {
|
|
140
137
|
if (newVal && prop.mode !== "edit") {
|
|
141
|
-
const
|
|
138
|
+
const fileId = Array.isArray(newVal) ? newVal[0] : newVal;
|
|
139
|
+
const fileData = await getFileById(fileId);
|
|
142
140
|
if (fileData) {
|
|
143
141
|
document.value.name = fileData.data.name;
|
|
144
142
|
}
|
|
@@ -146,6 +144,14 @@ watch(
|
|
|
146
144
|
}
|
|
147
145
|
);
|
|
148
146
|
|
|
147
|
+
function deriveTypeFromFilename(name = "") {
|
|
148
|
+
const n = String(name).toLowerCase();
|
|
149
|
+
if (n.endsWith(".pdf")) return "pdf";
|
|
150
|
+
if (n.endsWith(".csv")) return "csv";
|
|
151
|
+
if (n.endsWith(".doc") || n.endsWith(".docx")) return "doc";
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
|
|
149
155
|
function cancel() {
|
|
150
156
|
// createMore.value = false;
|
|
151
157
|
message.value = "";
|
|
@@ -156,23 +162,48 @@ async function submit() {
|
|
|
156
162
|
disable.value = true;
|
|
157
163
|
try {
|
|
158
164
|
if (prop.mode === "add") {
|
|
165
|
+
const fileId = Array.isArray(document.value.attachment)
|
|
166
|
+
? document.value.attachment[0]
|
|
167
|
+
: document.value.attachment;
|
|
168
|
+
|
|
169
|
+
const fileData = await getFileById(fileId);
|
|
170
|
+
const fileName = fileData?.data?.name || String(fileId || "");
|
|
171
|
+
const derivedType = deriveTypeFromFilename(fileName);
|
|
172
|
+
|
|
173
|
+
if (!derivedType) {
|
|
174
|
+
throw new Error("Unsupported file type. Allowed: pdf, csv, doc.");
|
|
175
|
+
}
|
|
176
|
+
|
|
159
177
|
const payload = {
|
|
160
178
|
name: document.value.name,
|
|
161
179
|
attachment: document.value.attachment,
|
|
162
|
-
type:
|
|
180
|
+
type: derivedType,
|
|
163
181
|
status: "active",
|
|
164
182
|
org: orgId,
|
|
165
183
|
site: siteId,
|
|
184
|
+
parentId: effectiveParentId.value,
|
|
166
185
|
};
|
|
186
|
+
|
|
167
187
|
await add(payload);
|
|
168
188
|
}
|
|
169
189
|
|
|
170
190
|
if (prop.mode === "edit") {
|
|
171
|
-
const
|
|
191
|
+
const fileId = Array.isArray(document.value.attachment)
|
|
192
|
+
? document.value.attachment[0]
|
|
193
|
+
: document.value.attachment;
|
|
194
|
+
|
|
195
|
+
let payload: Record<string, any> = {
|
|
172
196
|
name: document.value.name,
|
|
173
197
|
attachment: document.value.attachment,
|
|
174
|
-
type: document.value.type,
|
|
175
198
|
};
|
|
199
|
+
|
|
200
|
+
if (fileId) {
|
|
201
|
+
const fileData = await getFileById(fileId);
|
|
202
|
+
const fileName = fileData?.data?.name || String(fileId || "");
|
|
203
|
+
const derivedType = deriveTypeFromFilename(fileName);
|
|
204
|
+
if (derivedType) payload.type = derivedType;
|
|
205
|
+
}
|
|
206
|
+
|
|
176
207
|
await updateById(prop.document._id ?? "", payload);
|
|
177
208
|
}
|
|
178
209
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
<v-row no-gutters class="pa-0">
|
|
3
3
|
<v-col cols="12" class="mb-2">
|
|
4
4
|
<v-row no-gutters align="center">
|
|
5
|
-
<!-- Left Buttons -->
|
|
6
5
|
<v-col cols="12" md="6" class="d-flex align-center">
|
|
7
6
|
<v-btn
|
|
8
7
|
class="text-none me-2"
|
|
@@ -21,19 +20,17 @@
|
|
|
21
20
|
variant="tonal"
|
|
22
21
|
size="large"
|
|
23
22
|
@click="openCreateFolderDialog()"
|
|
24
|
-
v-if="canCreate &&
|
|
23
|
+
v-if="canCreate && canCreateFolder"
|
|
25
24
|
>
|
|
26
25
|
Create Folder
|
|
27
26
|
</v-btn>
|
|
28
27
|
</v-col>
|
|
29
28
|
|
|
30
|
-
<!-- Right Search + Filter -->
|
|
31
29
|
<v-col
|
|
32
30
|
cols="12"
|
|
33
31
|
md="6"
|
|
34
32
|
class="d-flex justify-end align-center mt-4 mt-md-0"
|
|
35
33
|
>
|
|
36
|
-
<!-- Search -->
|
|
37
34
|
<v-text-field
|
|
38
35
|
v-model="search"
|
|
39
36
|
placeholder="Search"
|
|
@@ -54,7 +51,7 @@
|
|
|
54
51
|
class="filtertype-btn text-none"
|
|
55
52
|
style="max-width: 220px"
|
|
56
53
|
>
|
|
57
|
-
{{ selectedFileType ||
|
|
54
|
+
{{ selectedFileType || "Filter by File Type" }}
|
|
58
55
|
</v-btn>
|
|
59
56
|
</template>
|
|
60
57
|
|
|
@@ -76,6 +73,22 @@
|
|
|
76
73
|
</v-col>
|
|
77
74
|
</v-row>
|
|
78
75
|
</v-col>
|
|
76
|
+
|
|
77
|
+
<v-col cols="12" class="mb-2" v-if="isInsideFolder">
|
|
78
|
+
<div class="folder-path-bar d-flex align-center ga-2 px-3 py-2">
|
|
79
|
+
<v-btn
|
|
80
|
+
icon="mdi-arrow-left"
|
|
81
|
+
size="small"
|
|
82
|
+
variant="text"
|
|
83
|
+
@click="goBackFolder"
|
|
84
|
+
/>
|
|
85
|
+
<v-icon size="18" color="grey-darken-2">mdi-folder</v-icon>
|
|
86
|
+
<span class="text-body-2 font-weight-medium">{{
|
|
87
|
+
currentFolderPath
|
|
88
|
+
}}</span>
|
|
89
|
+
</div>
|
|
90
|
+
</v-col>
|
|
91
|
+
|
|
79
92
|
<v-col cols="12">
|
|
80
93
|
<TableV3
|
|
81
94
|
:headers="headers"
|
|
@@ -89,11 +102,49 @@
|
|
|
89
102
|
@update:page="page = $event"
|
|
90
103
|
@row-click="tableRowClickHandler"
|
|
91
104
|
>
|
|
105
|
+
<template v-if="isInsideFolder" #breadcrumb>
|
|
106
|
+
<div class="d-flex align-center flex-wrap ga-1">
|
|
107
|
+
<v-btn
|
|
108
|
+
icon="mdi-chevron-left"
|
|
109
|
+
size="x-small"
|
|
110
|
+
variant="text"
|
|
111
|
+
density="comfortable"
|
|
112
|
+
class="me-1"
|
|
113
|
+
@click="goBackFolder"
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
<span
|
|
117
|
+
class="breadcrumb-segment text-body-2 text-grey-darken-1 cursor-pointer"
|
|
118
|
+
@click="navigateToRoot"
|
|
119
|
+
>Home</span
|
|
120
|
+
>
|
|
121
|
+
<template v-for="(folder, idx) in folderStack" :key="folder.id">
|
|
122
|
+
<v-icon size="14" color="grey-darken-1">mdi-chevron-right</v-icon>
|
|
123
|
+
<span
|
|
124
|
+
:class="[
|
|
125
|
+
'breadcrumb-segment text-body-2 font-weight-medium',
|
|
126
|
+
idx === folderStack.length - 1
|
|
127
|
+
? 'text-black'
|
|
128
|
+
: 'text-grey-darken-1 cursor-pointer',
|
|
129
|
+
]"
|
|
130
|
+
@click="
|
|
131
|
+
idx < folderStack.length - 1 && navigateToStackIndex(idx)
|
|
132
|
+
"
|
|
133
|
+
>{{ folder.name }}</span
|
|
134
|
+
>
|
|
135
|
+
</template>
|
|
136
|
+
</div>
|
|
137
|
+
</template>
|
|
92
138
|
<template #item.name="{ item }">
|
|
93
139
|
<div class="d-flex align-center ga-2">
|
|
94
|
-
<v-icon
|
|
140
|
+
<v-icon
|
|
141
|
+
size="18"
|
|
142
|
+
:color="
|
|
143
|
+
isFolderItem(item._raw) ? 'amber-darken-2' : 'grey-darken-2'
|
|
144
|
+
"
|
|
145
|
+
>
|
|
95
146
|
{{
|
|
96
|
-
item.
|
|
147
|
+
isFolderItem(item._raw)
|
|
97
148
|
? "mdi-folder"
|
|
98
149
|
: "mdi-file-document-outline"
|
|
99
150
|
}}
|
|
@@ -115,6 +166,7 @@
|
|
|
115
166
|
variant="flat"
|
|
116
167
|
class="table-download-btn text-none"
|
|
117
168
|
@click.stop="downloadDocument(item._raw)"
|
|
169
|
+
:disabled="isFolderItem(item._raw)"
|
|
118
170
|
>
|
|
119
171
|
Download
|
|
120
172
|
</v-btn>
|
|
@@ -124,7 +176,11 @@
|
|
|
124
176
|
|
|
125
177
|
<!-- Create Dialog -->
|
|
126
178
|
<v-dialog v-model="createDialog" width="450" persistent>
|
|
127
|
-
<DocumentForm
|
|
179
|
+
<DocumentForm
|
|
180
|
+
:parent-id="activeParentId"
|
|
181
|
+
@cancel="createDialog = false"
|
|
182
|
+
@success="successCreate()"
|
|
183
|
+
/>
|
|
128
184
|
</v-dialog>
|
|
129
185
|
|
|
130
186
|
<!-- Create Folder Dialog -->
|
|
@@ -145,7 +201,7 @@
|
|
|
145
201
|
<v-text-field
|
|
146
202
|
v-model.trim="folderTitle"
|
|
147
203
|
density="comfortable"
|
|
148
|
-
:rules="[v => !!v || 'Folder title is required']"
|
|
204
|
+
:rules="[(v) => !!v || 'Folder title is required']"
|
|
149
205
|
/>
|
|
150
206
|
</v-col>
|
|
151
207
|
|
|
@@ -394,6 +450,10 @@ const props = defineProps({
|
|
|
394
450
|
type: Boolean,
|
|
395
451
|
default: true,
|
|
396
452
|
},
|
|
453
|
+
canCreateFolder: {
|
|
454
|
+
type: Boolean,
|
|
455
|
+
default: true,
|
|
456
|
+
},
|
|
397
457
|
canUpdateDocument: {
|
|
398
458
|
type: Boolean,
|
|
399
459
|
default: true,
|
|
@@ -406,9 +466,17 @@ const props = defineProps({
|
|
|
406
466
|
|
|
407
467
|
const { headerSearch } = useLocal();
|
|
408
468
|
const documentService = useDocument();
|
|
409
|
-
const {
|
|
469
|
+
const {
|
|
470
|
+
getAll: _getAllDocuments,
|
|
471
|
+
deleteById,
|
|
472
|
+
createFolder,
|
|
473
|
+
create,
|
|
474
|
+
} = documentService;
|
|
410
475
|
const { getFileUrl } = useFile();
|
|
411
476
|
|
|
477
|
+
const router = useRouter();
|
|
478
|
+
const route = useRoute();
|
|
479
|
+
|
|
412
480
|
const page = ref(1);
|
|
413
481
|
const pages = ref(0);
|
|
414
482
|
const pageRange = ref("-- - -- of --");
|
|
@@ -419,8 +487,29 @@ const messageColor = ref("");
|
|
|
419
487
|
|
|
420
488
|
const items = ref<Array<Record<string, any>>>([]);
|
|
421
489
|
|
|
422
|
-
const route = useRoute();
|
|
423
490
|
const siteId = route.params.site as string;
|
|
491
|
+
const orgId = route.params.org as string;
|
|
492
|
+
const routeParentId = computed(() =>
|
|
493
|
+
typeof route.query.parentId === "string" ? route.query.parentId : ""
|
|
494
|
+
);
|
|
495
|
+
const activeParentId = ref(routeParentId.value);
|
|
496
|
+
const folderStack = ref<Array<{ id: string; name: string }>>([]);
|
|
497
|
+
|
|
498
|
+
const search = computed({
|
|
499
|
+
get: () => headerSearch.value,
|
|
500
|
+
set: (value: string) => {
|
|
501
|
+
headerSearch.value = value;
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const isInsideFolder = computed(() => activeParentId.value !== "");
|
|
506
|
+
const currentFolderPath = computed(() => {
|
|
507
|
+
if (folderStack.value.length === 0 && activeParentId.value) {
|
|
508
|
+
return "Current Folder";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return folderStack.value.map((folder) => folder.name).join(" / ");
|
|
512
|
+
});
|
|
424
513
|
|
|
425
514
|
const {
|
|
426
515
|
data: getDocumentReq,
|
|
@@ -433,9 +522,10 @@ const {
|
|
|
433
522
|
page: page.value,
|
|
434
523
|
search: headerSearch.value,
|
|
435
524
|
site: siteId,
|
|
525
|
+
parentId: activeParentId.value,
|
|
436
526
|
}),
|
|
437
527
|
{
|
|
438
|
-
watch: [page, headerSearch],
|
|
528
|
+
watch: [page, headerSearch, activeParentId],
|
|
439
529
|
}
|
|
440
530
|
);
|
|
441
531
|
|
|
@@ -473,8 +563,8 @@ const tableItems = computed(() => {
|
|
|
473
563
|
return items.value.map((doc: any) => ({
|
|
474
564
|
...doc,
|
|
475
565
|
_raw: doc,
|
|
476
|
-
file_type: doc.type || "N/A",
|
|
477
|
-
file_size: doc.file_size || doc.size || "0mb",
|
|
566
|
+
file_type: doc.type === "folder" ? "Folder" : doc.type || "N/A",
|
|
567
|
+
file_size: doc.type === "folder" ? "—" : doc.file_size || doc.size || "0mb",
|
|
478
568
|
date_created: formatDate(doc.created_at || doc.createdAt),
|
|
479
569
|
date_modified: formatDate(doc.updated_at || doc.updatedAt),
|
|
480
570
|
remarks: doc.remarks || "N/A",
|
|
@@ -506,10 +596,51 @@ const confirmDialog = ref(false);
|
|
|
506
596
|
const selectedDocumentId = ref<string | null>(null);
|
|
507
597
|
|
|
508
598
|
function tableRowClickHandler(data: any) {
|
|
509
|
-
|
|
599
|
+
const rowData = (data?._raw || data) as TDocument;
|
|
600
|
+
|
|
601
|
+
if (isFolderItem(rowData)) {
|
|
602
|
+
openFolder(rowData);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
selectedDocument.value = rowData;
|
|
510
607
|
previewDialog.value = true;
|
|
511
608
|
}
|
|
512
609
|
|
|
610
|
+
function isFolderItem(document?: Partial<TDocument> & Record<string, any>) {
|
|
611
|
+
if (!document) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const docType = String(
|
|
616
|
+
document.type || document.file_type || ""
|
|
617
|
+
).toLowerCase();
|
|
618
|
+
return docType === "folder";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function openFolder(document: Partial<TDocument> & Record<string, any>) {
|
|
622
|
+
const folderId = String(document._id || "");
|
|
623
|
+
const folderName = String(document.name || "Folder");
|
|
624
|
+
|
|
625
|
+
if (!folderId) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
activeParentId.value = folderId;
|
|
630
|
+
folderStack.value.push({ id: folderId, name: folderName });
|
|
631
|
+
page.value = 1;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function goBackFolder() {
|
|
635
|
+
if (folderStack.value.length === 0) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
folderStack.value.pop();
|
|
640
|
+
activeParentId.value = folderStack.value.at(-1)?.id || "";
|
|
641
|
+
page.value = 1;
|
|
642
|
+
}
|
|
643
|
+
|
|
513
644
|
function downloadDocument(document?: TDocument) {
|
|
514
645
|
if (!document?.attachment || !process.client) {
|
|
515
646
|
return;
|
|
@@ -563,24 +694,28 @@ async function handleCreateFolder() {
|
|
|
563
694
|
|
|
564
695
|
folderLoading.value = true;
|
|
565
696
|
try {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
697
|
+
const payload = {
|
|
698
|
+
name: folderTitle.value,
|
|
699
|
+
remarks: folderRemarks.value,
|
|
700
|
+
org: orgId,
|
|
701
|
+
site: siteId,
|
|
702
|
+
parentId: activeParentId.value,
|
|
703
|
+
type: "folder",
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
let resp: any = null;
|
|
707
|
+
if ((documentService as any).createFolder) {
|
|
708
|
+
resp = await (documentService as any).createFolder(payload);
|
|
573
709
|
} else if ((documentService as any).create) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
type: "folder",
|
|
578
|
-
site: siteId,
|
|
579
|
-
});
|
|
580
|
-
showMessage(resp?.message || "Folder created successfully!", "success");
|
|
710
|
+
resp = await (documentService as any).create(payload);
|
|
711
|
+
} else if ((documentService as any).add) {
|
|
712
|
+
resp = await (documentService as any).add(payload);
|
|
581
713
|
} else {
|
|
582
|
-
|
|
583
|
-
|
|
714
|
+
showMessage("Unable to create folder. No API available.", "error");
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (resp) {
|
|
718
|
+
showMessage(resp?.message || "Folder created successfully!", "success");
|
|
584
719
|
}
|
|
585
720
|
|
|
586
721
|
createFolderDialog.value = false;
|
|
@@ -588,7 +723,10 @@ async function handleCreateFolder() {
|
|
|
588
723
|
folderRemarks.value = "";
|
|
589
724
|
await getDocuments();
|
|
590
725
|
} catch (error: any) {
|
|
591
|
-
showMessage(
|
|
726
|
+
showMessage(
|
|
727
|
+
error?.response?._data?.message || "Failed to create folder",
|
|
728
|
+
"error"
|
|
729
|
+
);
|
|
592
730
|
} finally {
|
|
593
731
|
folderLoading.value = false;
|
|
594
732
|
}
|
|
@@ -670,4 +808,10 @@ function selectFileType(type: string) {
|
|
|
670
808
|
.filtertype-item:last-child {
|
|
671
809
|
border-bottom: none;
|
|
672
810
|
}
|
|
811
|
+
|
|
812
|
+
.folder-path-bar {
|
|
813
|
+
border: 1px solid #e0e0e0;
|
|
814
|
+
border-radius: 8px;
|
|
815
|
+
background: #fafafa;
|
|
816
|
+
}
|
|
673
817
|
</style>
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
</v-row>
|
|
75
75
|
</v-col>
|
|
76
76
|
|
|
77
|
-
<v-col
|
|
77
|
+
<v-col v-if="!props.hideCreateMore" cols="12" class="mt-2">
|
|
78
78
|
<v-checkbox v-model="createMore" density="comfortable" hide-details>
|
|
79
79
|
<template #label>
|
|
80
80
|
<span class="text-subtitle-2 font-weight-bold">
|
|
@@ -194,7 +194,11 @@ const props = defineProps({
|
|
|
194
194
|
inviteMember: {
|
|
195
195
|
type: Boolean,
|
|
196
196
|
default: false
|
|
197
|
-
}
|
|
197
|
+
},
|
|
198
|
+
hideCreateMore: {
|
|
199
|
+
type: Boolean,
|
|
200
|
+
default: false
|
|
201
|
+
}
|
|
198
202
|
});
|
|
199
203
|
|
|
200
204
|
const emit = defineEmits(["cancel", "success", "success:create-more"]);
|
|
@@ -221,8 +221,7 @@ const { requiredRule } = useUtils();
|
|
|
221
221
|
const { createRole } = useRole();
|
|
222
222
|
const { getSiteById } = useSite();
|
|
223
223
|
|
|
224
|
-
//
|
|
225
|
-
// để chỉ trigger 1 render cycle khi update cả 2
|
|
224
|
+
// All selected + enabled 1 shallowRef
|
|
226
225
|
const permState = shallowRef({
|
|
227
226
|
selected: [] as string[],
|
|
228
227
|
enabled: [] as string[],
|
|
@@ -283,7 +282,7 @@ function toggleSelectAll(value: boolean) {
|
|
|
283
282
|
}
|
|
284
283
|
}
|
|
285
284
|
|
|
286
|
-
// 1 assignment
|
|
285
|
+
// 1 assignment to trigger reactivity only once
|
|
287
286
|
permState.value = { selected: allKeys, enabled: categories }
|
|
288
287
|
}
|
|
289
288
|
|
package/components/TableV3.vue
CHANGED
|
@@ -43,6 +43,10 @@
|
|
|
43
43
|
</template>
|
|
44
44
|
</v-toolbar>
|
|
45
45
|
|
|
46
|
+
<div v-if="$slots.breadcrumb" class="table-v3__breadcrumb">
|
|
47
|
+
<slot name="breadcrumb" />
|
|
48
|
+
</div>
|
|
49
|
+
|
|
46
50
|
<v-data-table
|
|
47
51
|
:headers="headers"
|
|
48
52
|
:items="items"
|
|
@@ -176,4 +180,10 @@ watch(
|
|
|
176
180
|
width: 38px;
|
|
177
181
|
height: 38px;
|
|
178
182
|
}
|
|
183
|
+
|
|
184
|
+
.table-v3__breadcrumb {
|
|
185
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
186
|
+
background-color: #fff;
|
|
187
|
+
padding: 6px 12px;
|
|
188
|
+
}
|
|
179
189
|
</style>
|
|
@@ -299,7 +299,23 @@ export default function useAccessManagement() {
|
|
|
299
299
|
);
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
function getResidents(params: { orgId: string; siteId: string; unitId: string }) {
|
|
303
|
+
return useNuxtApp().$api<{ data: Record<string, any>[] }>(
|
|
304
|
+
`/api/access-management/residents`,
|
|
305
|
+
{ method: "GET", query: params }
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function assignUser(payload: { assignees: string[]; unit: string; type: string; acm_url: string }) {
|
|
310
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
311
|
+
`/api/access-management/assign-user`,
|
|
312
|
+
{ method: "POST", body: payload }
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
302
316
|
return {
|
|
317
|
+
getResidents,
|
|
318
|
+
assignUser,
|
|
303
319
|
getDoorAccessLevels,
|
|
304
320
|
getLiftAccessLevels,
|
|
305
321
|
getAccessGroups,
|
|
@@ -5,6 +5,7 @@ export default function useDocument() {
|
|
|
5
5
|
limit = 10,
|
|
6
6
|
status = "active",
|
|
7
7
|
site = "",
|
|
8
|
+
parentId = "",
|
|
8
9
|
} = {}) {
|
|
9
10
|
return useNuxtApp().$api<Record<string, any>>(`/api/documents`, {
|
|
10
11
|
method: "GET",
|
|
@@ -14,6 +15,7 @@ export default function useDocument() {
|
|
|
14
15
|
limit,
|
|
15
16
|
status,
|
|
16
17
|
site,
|
|
18
|
+
parentId,
|
|
17
19
|
},
|
|
18
20
|
});
|
|
19
21
|
}
|
|
@@ -26,10 +28,13 @@ export default function useDocument() {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
function deleteById(id: string) {
|
|
29
|
-
return useNuxtApp().$api<Record<string, any>>(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
32
|
+
`/api/documents/deleted/document`,
|
|
33
|
+
{
|
|
34
|
+
method: "PUT",
|
|
35
|
+
query: { id },
|
|
36
|
+
}
|
|
37
|
+
);
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function updateById(id: string, value: any) {
|
|
@@ -39,6 +44,12 @@ export default function useDocument() {
|
|
|
39
44
|
});
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
function getById(id: string) {
|
|
48
|
+
return useNuxtApp().$api<Record<string, any>>(`/api/documents/${id}`, {
|
|
49
|
+
method: "GET",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
function getDocumentsBySiteId(siteId: string, type?: string) {
|
|
43
54
|
return useNuxtApp().$api<Record<string, any>>(
|
|
44
55
|
`/api/documents/site/${siteId}/type/${type}`,
|
|
@@ -48,11 +59,22 @@ export default function useDocument() {
|
|
|
48
59
|
);
|
|
49
60
|
}
|
|
50
61
|
|
|
62
|
+
function create(value: any) {
|
|
63
|
+
return add(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createFolder(value: any) {
|
|
67
|
+
return add({ ...value, type: "folder" });
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
return {
|
|
52
71
|
getAll,
|
|
72
|
+
getById,
|
|
53
73
|
add,
|
|
54
74
|
deleteById,
|
|
55
75
|
updateById,
|
|
56
76
|
getDocumentsBySiteId,
|
|
77
|
+
create,
|
|
78
|
+
createFolder,
|
|
57
79
|
};
|
|
58
80
|
}
|
package/composables/useOrg.ts
CHANGED
|
@@ -107,31 +107,6 @@ export default function useOrg() {
|
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/** Super Admin client list, etc.: filter by `organizations.status` (v1 API ignores status). */
|
|
111
|
-
function getAllV2({
|
|
112
|
-
page = 1,
|
|
113
|
-
search = "",
|
|
114
|
-
limit = 20,
|
|
115
|
-
nature = "",
|
|
116
|
-
status = "active",
|
|
117
|
-
}: {
|
|
118
|
-
page?: number;
|
|
119
|
-
search?: string;
|
|
120
|
-
limit?: number;
|
|
121
|
-
nature?: string;
|
|
122
|
-
status?: "active" | "suspended" | "deleted";
|
|
123
|
-
} = {}) {
|
|
124
|
-
return useNuxtApp().$api<Record<string, any>>("/api/organizations/v2", {
|
|
125
|
-
method: "GET",
|
|
126
|
-
query: {
|
|
127
|
-
page,
|
|
128
|
-
search,
|
|
129
|
-
limit,
|
|
130
|
-
nature,
|
|
131
|
-
status,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
110
|
|
|
136
111
|
function add(
|
|
137
112
|
value: Partial<Pick<TOrg, "name" | "email" | "nature" | "contact">>
|
|
@@ -179,6 +154,64 @@ export default function useOrg() {
|
|
|
179
154
|
}
|
|
180
155
|
);
|
|
181
156
|
}
|
|
157
|
+
function getAllV2({
|
|
158
|
+
page = 1,
|
|
159
|
+
search = "",
|
|
160
|
+
limit = 20,
|
|
161
|
+
nature = "",
|
|
162
|
+
status = "active",
|
|
163
|
+
|
|
164
|
+
}: {
|
|
165
|
+
page?: number;
|
|
166
|
+
search?: string;
|
|
167
|
+
limit?: number;
|
|
168
|
+
nature?: string;
|
|
169
|
+
status?: "active" | "suspended" | "deleted";
|
|
170
|
+
} = {}) {
|
|
171
|
+
return useNuxtApp().$api<Record<string, any>>("/api/organizations/v2", {
|
|
172
|
+
method: "GET",
|
|
173
|
+
query: {
|
|
174
|
+
page,
|
|
175
|
+
search,
|
|
176
|
+
limit,
|
|
177
|
+
nature,
|
|
178
|
+
status,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getOrganizationsWithSubscription({
|
|
184
|
+
page = 1,
|
|
185
|
+
search = "",
|
|
186
|
+
limit = 20,
|
|
187
|
+
status = "active",
|
|
188
|
+
type = "",
|
|
189
|
+
billingCycle = "",
|
|
190
|
+
}: {
|
|
191
|
+
page?: number
|
|
192
|
+
search?: string
|
|
193
|
+
limit?: number
|
|
194
|
+
status?: "active" | "suspended" | "deleted"
|
|
195
|
+
type?: string
|
|
196
|
+
billingCycle?: string
|
|
197
|
+
} = {}) {
|
|
198
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
199
|
+
"/api/organizations/v2/orgs/subscriptions",
|
|
200
|
+
{
|
|
201
|
+
method: "GET",
|
|
202
|
+
|
|
203
|
+
query: {
|
|
204
|
+
page,
|
|
205
|
+
search,
|
|
206
|
+
limit,
|
|
207
|
+
status,
|
|
208
|
+
type,
|
|
209
|
+
billingCycle,
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
182
215
|
|
|
183
216
|
return {
|
|
184
217
|
org,
|
|
@@ -196,6 +229,7 @@ export default function useOrg() {
|
|
|
196
229
|
add,
|
|
197
230
|
addOnboardingOrg,
|
|
198
231
|
updateOrg,
|
|
199
|
-
getOrgsByEmail
|
|
232
|
+
getOrgsByEmail,
|
|
233
|
+
getOrganizationsWithSubscription
|
|
200
234
|
};
|
|
201
235
|
}
|
package/composables/usePeople.ts
CHANGED
|
@@ -14,7 +14,18 @@ export default function () {
|
|
|
14
14
|
} = {}) {
|
|
15
15
|
return await useNuxtApp().$api<Record<string, any>>("/api/people", {
|
|
16
16
|
method: "GET",
|
|
17
|
-
query: {
|
|
17
|
+
query: {
|
|
18
|
+
page,
|
|
19
|
+
limit,
|
|
20
|
+
sort,
|
|
21
|
+
search,
|
|
22
|
+
org,
|
|
23
|
+
site,
|
|
24
|
+
dateTo,
|
|
25
|
+
dateFrom,
|
|
26
|
+
type,
|
|
27
|
+
status,
|
|
28
|
+
},
|
|
18
29
|
});
|
|
19
30
|
}
|
|
20
31
|
|
|
@@ -26,12 +37,10 @@ export default function () {
|
|
|
26
37
|
});
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
nric: string, site: string
|
|
31
|
-
){
|
|
40
|
+
async function findPersonByNRICMultipleResult(nric: string, site: string) {
|
|
32
41
|
return await $fetch<Record<any, any>>(`/api/people/all-nric`, {
|
|
33
42
|
method: "GET",
|
|
34
|
-
query: { nric, site }
|
|
43
|
+
query: { nric, site },
|
|
35
44
|
});
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -98,6 +107,15 @@ export default function () {
|
|
|
98
107
|
}
|
|
99
108
|
);
|
|
100
109
|
}
|
|
110
|
+
async function reviewResidentPerson(
|
|
111
|
+
_id: string,
|
|
112
|
+
payload: Partial<TPeoplePayload>
|
|
113
|
+
) {
|
|
114
|
+
return await useNuxtApp().$api<Record<string, any>>(`/api/people/${_id}`, {
|
|
115
|
+
method: "PATCH",
|
|
116
|
+
body: payload,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
101
119
|
|
|
102
120
|
return {
|
|
103
121
|
create,
|
|
@@ -110,5 +128,6 @@ export default function () {
|
|
|
110
128
|
getPeopleByUnit,
|
|
111
129
|
searchCompanyList,
|
|
112
130
|
findUsersByPlateNumber,
|
|
131
|
+
reviewResidentPerson,
|
|
113
132
|
};
|
|
114
133
|
}
|
package/package.json
CHANGED
package/types/customer.d.ts
CHANGED
|
@@ -35,4 +35,10 @@ declare type TCustomerSite = {
|
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
type TSiteCategory =
|
|
39
|
+
| "residential"
|
|
40
|
+
| "commercial"
|
|
41
|
+
| "shopping_mall"
|
|
42
|
+
| "mix_development"
|
|
43
|
+
| "industrial"
|
|
44
|
+
| "co_working_co_living";
|
package/types/people.d.ts
CHANGED
|
@@ -25,6 +25,6 @@ declare type TPeople = {
|
|
|
25
25
|
declare type TPlateNumber = { plateNumber: string, recNo: string, _id?: string, status: "active" | "deleted" | "pending", type: TVehicleType };
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
declare type TPeoplePayload = Pick<TGuest, "name" | "block" | "level" | "unit" | "unitName" | "contact" | "plateNumber" | "nric" | "contact" | "remarks" | "org" | "site" | "start" | "end" | "type" | "isOwner">
|
|
28
|
+
declare type TPeoplePayload = Pick<TGuest, "name" | "block" | "level" | "unit" | "unitName" | "contact" | "plateNumber" | "nric" | "contact" | "remarks" | "org" | "site" | "start" | "end" | "type" | "isOwner"> & { status?: string }
|
|
29
29
|
|
|
30
30
|
declare type TPeopleType = "guest" | "resident" | "tenant"
|