@7365admin1/layer-common 1.11.46 → 1.11.48

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.11.48
4
+
5
+ ### Patch Changes
6
+
7
+ - 75d1ea7: add new nature business v2
8
+
9
+ ## 1.11.47
10
+
11
+ ### Patch Changes
12
+
13
+ - 3ca3d41: fix search, filter and add get orgs with subscription
14
+ - ecacc9d: update site category, get permissions by invitation
15
+
3
16
  ## 1.11.46
4
17
 
5
18
  ### 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"
@@ -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
@@ -395,10 +399,11 @@ const emit = defineEmits<{
395
399
  "update:selectedCardInUnit": [value: Record<string, any> | null];
396
400
  replace: [];
397
401
  delete: [];
402
+ refresh: [];
398
403
  "assign-to-person": [{ card: Record<string, any> | null; person: Record<string, any> | null }];
399
404
  }>();
400
405
 
401
- const { getResidents } = useAccessManagement();
406
+ const { getResidents, assignUser } = useAccessManagement();
402
407
 
403
408
  const historyDialog = ref(false);
404
409
  const showAssignForm = ref(false);
@@ -406,6 +411,18 @@ const assignPersonType = ref<string>("");
406
411
  const assignPerson = ref<Record<string, any> | null>(null);
407
412
  const peopleItems = ref<Record<string, any>[]>([]);
408
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
+ }
409
426
 
410
427
  const isSelectedCardAvailable = computed(() => {
411
428
  if (!props.selectedCardInUnit?._id) return false;
@@ -444,13 +461,19 @@ async function openAssignForm() {
444
461
  showAssignForm.value = true;
445
462
  assignPerson.value = null;
446
463
  assignPersonType.value = "";
464
+ assignError.value = "";
447
465
  peopleItems.value = [];
466
+ encryptedAcmUrl.value = "";
448
467
 
449
468
  if (!props.unit?._id || !props.orgId || !props.siteId) return;
450
469
  peopleLoading.value = true;
451
470
  try {
452
- const res = await getResidents({ orgId: props.orgId, siteId: props.siteId, unitId: props.unit._id });
453
- peopleItems.value = res?.data ?? [];
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 ?? "";
454
477
  } catch {
455
478
  peopleItems.value = [];
456
479
  } finally {
@@ -462,14 +485,44 @@ function cancelAssign() {
462
485
  showAssignForm.value = false;
463
486
  assignPerson.value = null;
464
487
  assignPersonType.value = "";
488
+ assignError.value = "";
465
489
  }
466
490
 
467
- function confirmAssign() {
468
- emit("assign-to-person", {
469
- card: props.selectedCardInUnit,
470
- person: assignPerson.value,
471
- });
472
- cancelAssign();
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
+ }
473
526
  }
474
527
 
475
528
  function toggleCard(card: Record<string, any>) {
@@ -116,6 +116,7 @@
116
116
  :org-id="orgId"
117
117
  @replace="openReplaceDialog()"
118
118
  @delete="openDeleteDialog()"
119
+ @refresh="handleAssignRefresh"
119
120
  />
120
121
 
121
122
  <!-- Replace Card Dialog -->
@@ -305,6 +306,15 @@ function setCard({ mode = "create", dialog = true, data = {} as TCard } = {}) {
305
306
  }
306
307
  }
307
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
+
308
318
  function successCreate() {
309
319
  createDialog.value = false;
310
320
  getCards();
@@ -30,7 +30,7 @@
30
30
  />
31
31
  </v-col>
32
32
 
33
- <v-col v-if="isANPR" cols="12">
33
+ <!-- <v-col v-if="isANPR" cols="12">
34
34
  <InputLabel class="text-capitalize" title="Category" required />
35
35
  <v-select
36
36
  v-model="camera.category"
@@ -38,7 +38,7 @@
38
38
  density="comfortable"
39
39
  :rules="[requiredRule]"
40
40
  />
41
- </v-col>
41
+ </v-col> -->
42
42
 
43
43
  <v-col v-if="isANPR" cols="12">
44
44
  <InputLabel class="text-capitalize" title="Type" required />
@@ -50,14 +50,14 @@
50
50
  />
51
51
  </v-col>
52
52
 
53
- <v-col v-if="isANPR" cols="12">
53
+ <!-- <v-col v-if="isANPR" cols="12">
54
54
  <InputLabel class="text-capitalize" title="Guard House" />
55
55
  <v-select
56
56
  v-model="camera.guardPost"
57
57
  :items="guardPostOptions"
58
58
  density="comfortable"
59
59
  />
60
- </v-col>
60
+ </v-col> -->
61
61
 
62
62
  <v-col v-if="isANPR" cols="12">
63
63
  <InputLabel class="text-capitalize" title="User" required />
@@ -231,6 +231,8 @@ const anprDirectionOptions = computed(() => [
231
231
  { title: "Entry", value: "entry" },
232
232
  { title: "Exit", value: "exit" },
233
233
  { title: "Both Entry and Exit", value: "both" },
234
+ { title: "Visitors", value: "visitors" },
235
+ { title: "Residents", value: "residents" },
234
236
  ]);
235
237
 
236
238
  function back() {
@@ -228,7 +228,7 @@ import useSubscription from '../composables/useSubscription'
228
228
  type TabStatus = 'active' | 'suspended' | 'pending'
229
229
 
230
230
  /* ── COMPOSABLES ── */
231
- const { getAllV2 } = useOrg()
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 sync với tab, dùng để filter + đổi tab qua select
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
- const res = await getAllV2({
343
- page: page.value,
344
- search: search.value,
345
- limit: limit.value,
346
- status,
347
- // truyền filter xuống API nếu backend hỗ trợ
348
- ...(filterPlan.value ? { app: filterPlan.value } : {}),
349
- ...(filterBilling.value ? { billingCycle: filterBilling.value } : {}),
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
- const orgs = res?.data?.items ?? res?.items ?? []
353
- const mapped = await Promise.all(orgs.map((org: any) => mapOrg(org, status)))
320
+ const orgs = res?.data?.items ?? res?.items ?? []
354
321
 
355
- items.value = []
356
- await nextTick()
357
- items.value = [...mapped]
358
- pages.value = res?.pages ?? 1
359
- totalItems.value = parseTotalItems(res?.pageRange, mapped.length)
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, // search hoạt động đúng ở pending
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 select đổi → sync tab ── */
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 khi click tab (chỉ 2 giá trị active/suspended)
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() // dùng search.value trực tiếp, không truyền param
437
+ fetchData()
425
438
  }, 300)
426
439
  }
427
440
 
428
441
  function handleFilterChange() {
429
- // filterStatus watch sẽ tự trigger fetchData nếu status đổi
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 PDF Files"
19
- accept=".pdf, .doc, .docx, .xls, .xlsx, .txt"
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 fileData = await getFileById(newVal);
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,50 @@ 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: document.value.type || "Other",
180
+ type: derivedType,
181
+ size: fileData?.data?.size ?? fileData?.data?.file_size ?? 0,
163
182
  status: "active",
164
183
  org: orgId,
165
184
  site: siteId,
185
+ parentId: effectiveParentId.value,
166
186
  };
187
+
167
188
  await add(payload);
168
189
  }
169
190
 
170
191
  if (prop.mode === "edit") {
171
- const payload = {
192
+ const fileId = Array.isArray(document.value.attachment)
193
+ ? document.value.attachment[0]
194
+ : document.value.attachment;
195
+
196
+ let payload: Record<string, any> = {
172
197
  name: document.value.name,
173
198
  attachment: document.value.attachment,
174
- type: document.value.type,
175
199
  };
200
+
201
+ if (fileId) {
202
+ const fileData = await getFileById(fileId);
203
+ const fileName = fileData?.data?.name || String(fileId || "");
204
+ const derivedType = deriveTypeFromFilename(fileName);
205
+ if (derivedType) payload.type = derivedType;
206
+ payload.size = fileData?.data?.size ?? fileData?.data?.file_size ?? 0;
207
+ }
208
+
176
209
  await updateById(prop.document._id ?? "", payload);
177
210
  }
178
211