@7365admin1/layer-common 1.11.44 → 1.11.45

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.
@@ -1,58 +1,125 @@
1
1
  <template>
2
- <v-row no-gutters>
2
+ <v-row no-gutters class="pa-0">
3
3
  <v-col cols="12" class="mb-2">
4
- <v-row no-gutters>
5
- <v-btn
6
- class="text-none"
7
- rounded="pill"
8
- variant="tonal"
9
- size="large"
10
- @click="setDocument()"
11
- v-if="canCreate && canCreateDocument"
4
+ <v-row no-gutters align="center">
5
+ <!-- Left Buttons -->
6
+ <v-col cols="12" md="6" class="d-flex align-center">
7
+ <v-btn
8
+ class="text-none me-2"
9
+ rounded="pill"
10
+ variant="tonal"
11
+ size="large"
12
+ @click="setDocument()"
13
+ v-if="canCreate && canCreateDocument"
14
+ >
15
+ Add Document
16
+ </v-btn>
17
+
18
+ <v-btn
19
+ class="text-none"
20
+ rounded="pill"
21
+ variant="tonal"
22
+ size="large"
23
+ @click="openCreateFolderDialog()"
24
+ v-if="canCreate && canCreateDocument"
25
+ >
26
+ Create Folder
27
+ </v-btn>
28
+ </v-col>
29
+
30
+ <!-- Right Search + Filter -->
31
+ <v-col
32
+ cols="12"
33
+ md="6"
34
+ class="d-flex justify-end align-center mt-4 mt-md-0"
12
35
  >
13
- Add Document
14
- </v-btn>
36
+ <!-- Search -->
37
+ <v-text-field
38
+ v-model="search"
39
+ placeholder="Search"
40
+ density="comfortable"
41
+ variant="outlined"
42
+ hide-details
43
+ rounded="lg"
44
+ class="me-3"
45
+ style="max-width: 220px"
46
+ />
47
+
48
+ <v-menu v-model="filterMenu" offset-y>
49
+ <template #activator="{ props }">
50
+ <v-btn
51
+ v-bind="props"
52
+ variant="outlined"
53
+ rounded="lg"
54
+ class="filtertype-btn text-none"
55
+ style="max-width: 220px"
56
+ >
57
+ {{ selectedFileType || 'Filter by File Type' }}
58
+ </v-btn>
59
+ </template>
60
+
61
+ <v-card class="filtertype-card">
62
+ <v-list class="pa-0">
63
+ <v-list-item
64
+ v-for="(type, idx) in fileTypes"
65
+ :key="idx"
66
+ @click="selectFileType(type)"
67
+ class="filtertype-item"
68
+ >
69
+ <v-list-item-title class="text-subtitle-2">
70
+ {{ type }}
71
+ </v-list-item-title>
72
+ </v-list-item>
73
+ </v-list>
74
+ </v-card>
75
+ </v-menu>
76
+ </v-col>
15
77
  </v-row>
16
78
  </v-col>
17
79
  <v-col cols="12">
18
- <v-card
19
- width="100%"
20
- variant="outlined"
21
- border="thin"
22
- rounded="lg"
80
+ <TableV3
81
+ :headers="headers"
82
+ :items="tableItems"
23
83
  :loading="loading"
84
+ :page="page"
85
+ :pages="pages"
86
+ :page-range="pageRange"
87
+ :items-per-page="10"
88
+ @refresh="getDocuments"
89
+ @update:page="page = $event"
90
+ @row-click="tableRowClickHandler"
24
91
  >
25
- <v-toolbar density="compact" color="grey-lighten-4">
26
- <template #prepend>
27
- <v-btn fab icon density="comfortable" @click="getDocuments">
28
- <v-icon>mdi-refresh</v-icon>
29
- </v-btn>
30
- </template>
31
-
32
- <template #append>
33
- <v-row no-gutters justify="end" align="center">
34
- <span class="mr-2 text-caption text-fontgray">
35
- {{ pageRange }}
36
- </span>
37
- <local-pagination
38
- v-model="page"
39
- :length="pages"
40
- @update:value="getDocuments"
41
- />
42
- </v-row>
43
- </template>
44
- </v-toolbar>
45
- <v-data-table
46
- :headers="headers"
47
- :items="items"
48
- item-value="_id"
49
- items-per-page="10"
50
- fixed-header
51
- hide-default-footer
52
- @click:row="tableRowClickHandler"
53
- style="max-height: calc(100vh - (200px))"
54
- ></v-data-table>
55
- </v-card>
92
+ <template #item.name="{ item }">
93
+ <div class="d-flex align-center ga-2">
94
+ <v-icon size="18" color="grey-darken-2">
95
+ {{
96
+ item.file_type?.toLowerCase() === "folder"
97
+ ? "mdi-folder"
98
+ : "mdi-file-document-outline"
99
+ }}
100
+ </v-icon>
101
+ <span>{{ item.name }}</span>
102
+ </div>
103
+ </template>
104
+
105
+ <template #item.date_modified="{ item }">
106
+ <span>{{ item.date_modified || "N/A" }}</span>
107
+ </template>
108
+
109
+ <template #item.remarks="{ item }">
110
+ <span>{{ item.remarks || "N/A" }}</span>
111
+ </template>
112
+
113
+ <template #item.action="{ item }">
114
+ <v-btn
115
+ variant="flat"
116
+ class="table-download-btn text-none"
117
+ @click.stop="downloadDocument(item._raw)"
118
+ >
119
+ Download
120
+ </v-btn>
121
+ </template>
122
+ </TableV3>
56
123
  </v-col>
57
124
 
58
125
  <!-- Create Dialog -->
@@ -60,6 +127,73 @@
60
127
  <DocumentForm @cancel="createDialog = false" @success="successCreate()" />
61
128
  </v-dialog>
62
129
 
130
+ <!-- Create Folder Dialog -->
131
+ <v-dialog v-model="createFolderDialog" width="450" persistent>
132
+ <v-card width="100%">
133
+ <v-toolbar class="pa-0" density="compact">
134
+ <v-row no-gutters class="w-100">
135
+ <v-col cols="12" class="pa-4">
136
+ <span class="font-weight-medium text-h6">Create Folder</span>
137
+ </v-col>
138
+ </v-row>
139
+ </v-toolbar>
140
+
141
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-0">
142
+ <v-form v-model="validFolderForm">
143
+ <v-col cols="12" class="px-6">
144
+ <InputLabel title="Folder Title" />
145
+ <v-text-field
146
+ v-model.trim="folderTitle"
147
+ density="comfortable"
148
+ :rules="[v => !!v || 'Folder title is required']"
149
+ />
150
+ </v-col>
151
+
152
+ <v-col cols="12" class="px-6">
153
+ <InputLabel title="Remarks (Optional)" />
154
+ <v-textarea
155
+ v-model.trim="folderRemarks"
156
+ density="comfortable"
157
+ rows="3"
158
+ />
159
+ </v-col>
160
+ </v-form>
161
+ </v-card-text>
162
+
163
+ <v-toolbar class="pa-0" density="compact">
164
+ <v-row no-gutters class="w-100">
165
+ <v-col cols="6" class="pa-0">
166
+ <v-btn
167
+ block
168
+ variant="text"
169
+ class="text-none"
170
+ size="large"
171
+ @click="createFolderDialog = false"
172
+ :disabled="folderLoading"
173
+ height="48"
174
+ >
175
+ Cancel
176
+ </v-btn>
177
+ </v-col>
178
+ <v-col cols="6" class="pa-0">
179
+ <v-btn
180
+ block
181
+ color="black"
182
+ variant="flat"
183
+ class="text-none"
184
+ size="large"
185
+ @click="handleCreateFolder"
186
+ :loading="folderLoading"
187
+ height="48"
188
+ >
189
+ Submit
190
+ </v-btn>
191
+ </v-col>
192
+ </v-row>
193
+ </v-toolbar>
194
+ </v-card>
195
+ </v-dialog>
196
+
63
197
  <!-- Edit Dialog -->
64
198
  <v-dialog v-model="editDialog" width="450" persistent>
65
199
  <DocumentForm
@@ -218,10 +352,30 @@ const props = defineProps({
218
352
  value: "name",
219
353
  },
220
354
  {
221
- title: "Type",
222
- value: "type",
355
+ title: "File Type",
356
+ value: "file_type",
357
+ },
358
+ {
359
+ title: "File Size",
360
+ value: "file_size",
361
+ },
362
+ {
363
+ title: "Date Created",
364
+ value: "date_created",
365
+ },
366
+ {
367
+ title: "Date Modified",
368
+ value: "date_modified",
369
+ },
370
+ {
371
+ title: "Remarks",
372
+ value: "remarks",
373
+ },
374
+ {
375
+ title: "Action",
376
+ value: "action",
377
+ sortable: false,
223
378
  },
224
- // { title: "Action", value: "action-table" },
225
379
  ],
226
380
  },
227
381
  canCreate: {
@@ -251,7 +405,8 @@ const props = defineProps({
251
405
  });
252
406
 
253
407
  const { headerSearch } = useLocal();
254
- const { getAll: _getAllDocuments, deleteById } = useDocument();
408
+ const documentService = useDocument();
409
+ const { getAll: _getAllDocuments, deleteById, createFolder, create } = documentService;
255
410
  const { getFileUrl } = useFile();
256
411
 
257
412
  const page = ref(1);
@@ -294,9 +449,52 @@ watchEffect(() => {
294
449
  }
295
450
  });
296
451
 
452
+ function formatDate(value?: string | null) {
453
+ if (!value) {
454
+ return "N/A";
455
+ }
456
+
457
+ const dateValue = new Date(value);
458
+ if (Number.isNaN(dateValue.getTime())) {
459
+ return "N/A";
460
+ }
461
+
462
+ return dateValue.toLocaleString("en-SG", {
463
+ year: "numeric",
464
+ month: "2-digit",
465
+ day: "2-digit",
466
+ hour: "2-digit",
467
+ minute: "2-digit",
468
+ hour12: false,
469
+ });
470
+ }
471
+
472
+ const tableItems = computed(() => {
473
+ return items.value.map((doc: any) => ({
474
+ ...doc,
475
+ _raw: doc,
476
+ file_type: doc.type || "N/A",
477
+ file_size: doc.file_size || doc.size || "0mb",
478
+ date_created: formatDate(doc.created_at || doc.createdAt),
479
+ date_modified: formatDate(doc.updated_at || doc.updatedAt),
480
+ remarks: doc.remarks || "N/A",
481
+ }));
482
+ });
483
+
297
484
  const createDialog = ref(false);
298
485
  const editDialog = ref(false);
299
486
  const previewDialog = ref(false);
487
+
488
+ // Create folder dialog state
489
+ const createFolderDialog = ref(false);
490
+ const folderTitle = ref("");
491
+ const folderRemarks = ref("");
492
+ const folderLoading = ref(false);
493
+ const validFolderForm = ref(false);
494
+
495
+ const fileTypes = ref(["All", "pdf", "csv", "doc"]);
496
+ const selectedFileType = ref("All");
497
+ const filterMenu = ref(false);
300
498
  const selectedDocument = ref<TDocument>({
301
499
  _id: "",
302
500
  name: "",
@@ -307,11 +505,22 @@ const deleteLoading = ref(false);
307
505
  const confirmDialog = ref(false);
308
506
  const selectedDocumentId = ref<string | null>(null);
309
507
 
310
- function tableRowClickHandler(_: any, data: any) {
311
- selectedDocument.value = data.item as TDocument;
508
+ function tableRowClickHandler(data: any) {
509
+ selectedDocument.value = (data?._raw || data) as TDocument;
312
510
  previewDialog.value = true;
313
511
  }
314
512
 
513
+ function downloadDocument(document?: TDocument) {
514
+ if (!document?.attachment || !process.client) {
515
+ return;
516
+ }
517
+
518
+ const fileUrl = getFileUrl(document.attachment);
519
+ if (fileUrl) {
520
+ window.open(fileUrl, "_blank", "noopener,noreferrer");
521
+ }
522
+ }
523
+
315
524
  function setDocument({
316
525
  mode = "create",
317
526
  dialog = true,
@@ -340,6 +549,51 @@ function successCreate() {
340
549
  showMessage("Document created successfully!", "success");
341
550
  }
342
551
 
552
+ function openCreateFolderDialog() {
553
+ folderTitle.value = "";
554
+ folderRemarks.value = "";
555
+ createFolderDialog.value = true;
556
+ }
557
+
558
+ async function handleCreateFolder() {
559
+ if (!folderTitle.value) {
560
+ showMessage("Folder title is required", "error");
561
+ return;
562
+ }
563
+
564
+ folderLoading.value = true;
565
+ try {
566
+ if (documentService.createFolder) {
567
+ const resp = await (documentService as any).createFolder({
568
+ name: folderTitle.value,
569
+ remarks: folderRemarks.value,
570
+ site: siteId,
571
+ });
572
+ showMessage(resp?.message || "Folder created successfully!", "success");
573
+ } else if ((documentService as any).create) {
574
+ const resp = await (documentService as any).create({
575
+ name: folderTitle.value,
576
+ remarks: folderRemarks.value,
577
+ type: "folder",
578
+ site: siteId,
579
+ });
580
+ showMessage(resp?.message || "Folder created successfully!", "success");
581
+ } else {
582
+ // fallback: no API available in this service
583
+ showMessage("Folder created (local)", "success");
584
+ }
585
+
586
+ createFolderDialog.value = false;
587
+ folderTitle.value = "";
588
+ folderRemarks.value = "";
589
+ await getDocuments();
590
+ } catch (error: any) {
591
+ showMessage(error?.response?._data?.message || "Failed to create folder", "error");
592
+ } finally {
593
+ folderLoading.value = false;
594
+ }
595
+ }
596
+
343
597
  function openDeleteDialog() {
344
598
  confirmDialog.value = true;
345
599
  message.value = "";
@@ -372,4 +626,48 @@ function successUpdate() {
372
626
  getDocuments();
373
627
  showMessage("Document updated successfully!", "success");
374
628
  }
629
+
630
+ function selectFileType(type: string) {
631
+ selectedFileType.value = type;
632
+ filterMenu.value = false;
633
+ }
375
634
  </script>
635
+
636
+ <style scoped>
637
+ .table-download-btn {
638
+ min-width: 106px;
639
+ height: 30px;
640
+ border-radius: 4px;
641
+ background-color: #e3e3e3;
642
+ color: #38475a;
643
+ font-size: 12px;
644
+ font-weight: 500;
645
+ box-shadow: none;
646
+ }
647
+
648
+ .table-download-btn:hover {
649
+ background-color: #d8d8d8;
650
+ }
651
+
652
+ .filtertype-btn {
653
+ justify-content: flex-start;
654
+ width: 220px;
655
+ }
656
+
657
+ .filtertype-card {
658
+ background: #fff;
659
+ border-radius: 8px;
660
+ min-width: 160px;
661
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
662
+ overflow: hidden;
663
+ }
664
+
665
+ .filtertype-item {
666
+ border-bottom: 1px solid #f0f0f0;
667
+ padding: 10px 16px;
668
+ }
669
+
670
+ .filtertype-item:last-child {
671
+ border-bottom: none;
672
+ }
673
+ </style>
@@ -321,7 +321,7 @@ async function checkEmail(email: string) {
321
321
 
322
322
  const memberRes = await getAllByUserId(user._id)
323
323
 
324
- userAccess.value = memberRes?.items || []
324
+ userAccess.value = memberRes?.items || []
325
325
  } else {
326
326
  existingUser.value = null
327
327
  userAccess.value = []
@@ -340,23 +340,36 @@ async function submit() {
340
340
  submitError.value = ""
341
341
 
342
342
  try {
343
- await inviteUserClient({
344
- email: invite.value.email,
345
- app: invite.value.app,
346
- role: invite.value.role,
347
- org: org.value?._id,
348
- site: invite.value.site,
349
- siteName: invite.value.siteName,
350
- })
343
+
344
+ for (const app of invite.value.app) {
345
+
346
+ await inviteUserClient({
347
+ email: invite.value.email,
348
+
349
+ app: [app],
350
+
351
+ role: invite.value.role,
352
+
353
+ org: org.value?._id,
354
+
355
+ siteId: invite.value.site,
356
+ siteName: invite.value.siteName,
357
+ })
358
+ }
351
359
 
352
360
  emit("success", true)
353
361
 
354
362
  } catch (e: any) {
355
- submitError.value = e?.response?._data?.message || "Error"
363
+
364
+ submitError.value =
365
+ e?.response?._data?.message || "Error"
366
+
356
367
  } finally {
368
+
357
369
  loading.submittingForm = false
358
370
  }
359
371
  }
372
+
360
373
  function cancel() {
361
374
  emit("cancel")
362
375
  }
@@ -0,0 +1,179 @@
1
+ <template>
2
+ <v-card
3
+ width="100%"
4
+ variant="outlined"
5
+ border="thin"
6
+ rounded="lg"
7
+ :loading="loading"
8
+ class="table-v3"
9
+ >
10
+ <v-toolbar density="compact" color="grey-lighten-5">
11
+ <template #prepend>
12
+ <v-btn
13
+ icon
14
+ variant="text"
15
+ density="comfortable"
16
+ class="table-v3__refresh-btn"
17
+ @click="emit('refresh')"
18
+ >
19
+ <v-icon size="28" color="black">mdi-history</v-icon>
20
+ </v-btn>
21
+ </template>
22
+
23
+ <template #append>
24
+ <v-row no-gutters justify="end" align="center">
25
+ <span class="mr-2 text-caption text-fontgray">
26
+ {{ pageRange }}
27
+ </span>
28
+ <v-btn
29
+ icon="mdi-chevron-left"
30
+ variant="text"
31
+ density="comfortable"
32
+ :disabled="!canGoPrev"
33
+ @click="goPrev"
34
+ />
35
+ <v-btn
36
+ icon="mdi-chevron-right"
37
+ variant="text"
38
+ density="comfortable"
39
+ :disabled="!canGoNext"
40
+ @click="goNext"
41
+ />
42
+ </v-row>
43
+ </template>
44
+ </v-toolbar>
45
+
46
+ <v-data-table
47
+ :headers="headers"
48
+ :items="items"
49
+ :item-value="itemValue"
50
+ :items-per-page="itemsPerPage"
51
+ fixed-header
52
+ hide-default-footer
53
+ :style="`max-height: calc(100vh - (${offset}px))`"
54
+ @click:row="(_: any, data: any) => emit('row-click', data.item)"
55
+ >
56
+ <template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
57
+ <slot :name="slotName" v-bind="slotProps" />
58
+ </template>
59
+ </v-data-table>
60
+
61
+ <v-toolbar density="compact" color="grey-lighten-5" class="table-v3__footer">
62
+ <template #append>
63
+ <v-row no-gutters justify="end" align="center">
64
+ <span class="mr-2 text-caption text-fontgray">
65
+ {{ pageRange }}
66
+ </span>
67
+ <v-btn
68
+ icon="mdi-chevron-left"
69
+ variant="text"
70
+ density="comfortable"
71
+ :disabled="!canGoPrev"
72
+ @click="goPrev"
73
+ />
74
+ <v-btn
75
+ icon="mdi-chevron-right"
76
+ variant="text"
77
+ density="comfortable"
78
+ :disabled="!canGoNext"
79
+ @click="goNext"
80
+ />
81
+ </v-row>
82
+ </template>
83
+ </v-toolbar>
84
+ </v-card>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ const props = defineProps({
89
+ headers: {
90
+ type: Array as PropType<Array<Record<string, any>>>,
91
+ required: true,
92
+ },
93
+ items: {
94
+ type: Array as PropType<Array<Record<string, any>>>,
95
+ default: () => [],
96
+ },
97
+ loading: {
98
+ type: Boolean,
99
+ default: false,
100
+ },
101
+ itemValue: {
102
+ type: String,
103
+ default: "_id",
104
+ },
105
+ itemsPerPage: {
106
+ type: Number,
107
+ default: 20,
108
+ },
109
+ page: {
110
+ type: Number,
111
+ default: 1,
112
+ },
113
+ pages: {
114
+ type: Number,
115
+ default: 0,
116
+ },
117
+ pageRange: {
118
+ type: String,
119
+ default: "-- - -- of --",
120
+ },
121
+ offset: {
122
+ type: Number,
123
+ default: 220,
124
+ },
125
+ });
126
+
127
+ const emit = defineEmits(["refresh", "update:page", "row-click"]);
128
+
129
+ const internalPage = ref(props.page);
130
+ const canGoPrev = computed(() => internalPage.value > 1);
131
+ const canGoNext = computed(
132
+ () => props.pages > 0 && internalPage.value < props.pages
133
+ );
134
+
135
+ function goPrev() {
136
+ if (!canGoPrev.value) {
137
+ return;
138
+ }
139
+ internalPage.value -= 1;
140
+ emit("update:page", internalPage.value);
141
+ }
142
+
143
+ function goNext() {
144
+ if (!canGoNext.value) {
145
+ return;
146
+ }
147
+ internalPage.value += 1;
148
+ emit("update:page", internalPage.value);
149
+ }
150
+
151
+ watch(
152
+ () => props.page,
153
+ (val) => {
154
+ internalPage.value = val;
155
+ }
156
+ );
157
+ </script>
158
+
159
+ <style scoped>
160
+ .table-v3 :deep(thead th) {
161
+ background-color: #f5f5f5;
162
+ color: #6b7280;
163
+ font-weight: 600 !important;
164
+ font-size: 12px;
165
+ }
166
+
167
+ .table-v3 :deep(tbody td) {
168
+ height: 54px;
169
+ }
170
+
171
+ .table-v3__footer {
172
+ border-top: 1px solid rgba(0, 0, 0, 0.08);
173
+ }
174
+
175
+ .table-v3__refresh-btn {
176
+ width: 38px;
177
+ height: 38px;
178
+ }
179
+ </style>